Skip to content

Commit 7c427c8

Browse files
authored
Merge pull request #6865 from cardstack/cs-14147-scope-a-dedicated-ember-build-for-the-secure-origins-pages
Propose a dedicated Ember package for the secure origin's pages
2 parents 9329a7b + c5ae374 commit 7c427c8

3 files changed

Lines changed: 206 additions & 1 deletion

File tree

‎.claude/skills/secure-origin/SKILL.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ description: What the secure origin (`secure.boxel.ai` / `secure.stack.cards`, `
1515

1616
- **The credential fields**, at `credentials` beneath the shell's path: `credentials.html`, its entry point `credentials.js` and `credentials.css` (`CREDENTIALS_ASSETS`), and the shell's typeface. See below.
1717

18-
Adding to it means adding a mode to `shell.js`, or a file to `SHELL_ASSETS` or `CREDENTIALS_ASSETS`. Each addition must be code this repo owns and reviews. Never add a proxy, a passthrough to realm routing, a CDN script, an analytics snippet or a host-app import.
18+
Adding a screen means adding it to `packages/secure-origin`, as [ADR 008](../../../docs/adr/008-build-the-secure-origins-pages-as-their-own-ember-app.md) sets out. Until that package exists, no new mode goes into `shell.js`: new secure-window screens, such as key management, wait for the package scaffold. Each addition must be code this repo owns and reviews. Never add a proxy, a passthrough to realm routing, a CDN script, an analytics snippet or a host-app import.
19+
20+
The pages are deliberately not host components. ADR 008 records why, and where they are going: a package of their own, an Ember app with its own components, behind an import allowlist. Until that package exists, the plain pages are the gaps its Remediation section lists. Fixing or changing an existing mode in place is fine. A new screen would add a gap (ADR 001 §1.9), so it waits for the package. Don't add a framework, a build step or a dependency to the plain pages.
1921

2022
## The credential fields the app frames
2123

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
status: proposed
3+
date: 2026-10-20
4+
scope:
5+
- packages/secure-origin/**
6+
- packages/realm-server/secure-origin/**
7+
- packages/realm-server/lib/secure-origin.ts
8+
- packages/realm-server/realm-server.Dockerfile
9+
- packages/realm-server/tests/helpers/index.ts
10+
- packages/matrix/support/isolated-realm-server.ts
11+
- mise-tasks/services/realm-server
12+
- mise-tasks/test-services/**
13+
- .github/workflows/*secure-origin*
14+
- packages/host/app/components/matrix/auth-*.gts
15+
- packages/host/app/components/matrix/login.gts
16+
- packages/host/app/components/matrix/register-user.gts
17+
- packages/host/app/components/matrix/forgot-password.gts
18+
- packages/boxel-ui/src/styles/variables.css
19+
- packages/boxel-ui/src/styles/theme.css
20+
---
21+
22+
# 008. Build the secure origin's pages as their own Ember app
23+
24+
Every other screen in Boxel is a component in the host app. The secure
25+
origin's pages (the secure window, and the credential fields the app frames)
26+
are the deliberate exception. One script served anywhere on the secure origin
27+
can read every password typed on it, so all the code the origin serves is
28+
trusted code. It has to be small enough to review, and nothing else may change
29+
it. Plain JavaScript keeps it small, but it doesn't scale to the screens the
30+
secure window is gaining, such as key management. So the pages get a package
31+
of their own: an Ember app with its own components, an import allowlist
32+
enforced by the build, and a reviewed record of everything it ships. This ADR
33+
records that boundary and why the pages don't follow the rest of the UI
34+
architecture.
35+
36+
## 1. Decision
37+
38+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
39+
"SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this
40+
document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when,
41+
and only when, they appear in all capitals, as shown here.
42+
43+
### 1.1. A package of their own
44+
45+
The secure origin's pages MUST be built from their own package,
46+
`packages/secure-origin`. The realm server MUST serve on the secure origin only
47+
that package's build output, and the files allowed by §1.4. The build MUST run
48+
before any process that serves the secure origin starts, including the
49+
realm-server image, the development and test stacks, and test helpers that
50+
start the secure origin in-process. The realm server MUST NOT build anything
51+
at request time.
52+
53+
### 1.2. Ember, with templates compiled at build time
54+
55+
The pages SHOULD be an Ember app, written as Glimmer components. Templates
56+
MUST be compiled at build time. The build MUST NOT include Ember's runtime
57+
template compiler.
58+
59+
### 1.3. What the package may import
60+
61+
The package MUST NOT import from `packages/host`, `@cardstack/runtime-common`,
62+
the base realm, any realm or card module, or any script from
63+
`@cardstack/boxel-ui`. Every module in the build's output MUST come from the
64+
package itself or from a package named in its allowlist. A plugin in the
65+
package's build MUST fail the build when the output holds any other module,
66+
or any module of Ember's runtime template compiler (§1.2). Adding a package to
67+
the allowlist is a change to the secure origin's trusted code, and MUST be
68+
reviewed as one.
69+
70+
### 1.4. Styles
71+
72+
The pages MAY use boxel-ui's `variables.css`, `theme.css` and typeface,
73+
served as files, so their tokens are the app's. They MUST NOT load any other
74+
boxel-ui file.
75+
76+
### 1.5. The policy stays as it is
77+
78+
The build output MUST run under the secure origin's Content Security Policy
79+
unchanged, with no `unsafe-inline` and no `unsafe-eval`. Templates MUST NOT
80+
bind or set a `style` attribute. The package's template lint MUST enable
81+
`no-inline-styles` with `allowDynamicStyles: false`. Code that styles an
82+
element at run time MUST use the CSSOM (`element.style.setProperty`). Scoped
83+
CSS MUST be emitted as files. Entry documents MUST hold no inline script or
84+
style. A test MUST load each built page under the policy and fail on any
85+
`securitypolicyviolation` event. The secure origin MUST NOT be served by a
86+
development server that injects styles inline.
87+
88+
### 1.6. Security logic stays plain modules
89+
90+
The credential frame's message protocol and fixed forms, the Matrix requests,
91+
the session handling, origin matching and response redaction MUST live in
92+
`packages/secure-origin/src/security/`. Modules there MUST import nothing from
93+
Ember, Glimmer or the package's components, and a lint rule
94+
(`no-restricted-imports`) on that directory MUST enforce it. Components call
95+
these modules. They MUST be testable without rendering.
96+
97+
### 1.7. Serving
98+
99+
The build MUST emit one entry document for the shell and one for the
100+
credential fields, and a manifest that maps each entry to the files it loads.
101+
The realm server MUST read the manifest and the files once, at startup. The
102+
shell's path is configured per environment (`/` deployed, `/secure` in
103+
development). So the build MUST write asset URLs under a fixed placeholder
104+
base, and the realm server MUST replace that base with the configured path
105+
when it reads the files. The credential fields SHOULD have their own entry and
106+
bundle. Framing and headers MUST stay per path, as `secureOriginHeaders()`
107+
sets them.
108+
109+
### 1.8. Reviewing what ships
110+
111+
The package MUST commit a record of its build output: each module's package
112+
name and version, and its size. CI MUST rebuild the package and fail when the
113+
output differs from the record. So a bump to Ember or any allowed package
114+
shows, in the pull request that makes it, exactly what changes on the origin.
115+
116+
### 1.9. One implementation of the sign-in screens
117+
118+
The host SHOULD render its own sign-in, registration and password-reset
119+
screens from the package's presentational components, so that one
120+
implementation serves both origins. The host MAY import from the package. The
121+
package MUST NOT import from the host (§1.3).
122+
123+
## 2. Rationale
124+
125+
The rest of the UI is the host app, and new screens are expected to follow
126+
it. The secure origin can't. It exists so that card code in the app page
127+
can't read a credential, and the browser's same-origin policy does the
128+
isolating. That protects the origin, not a page on it, so every script the
129+
origin serves can read every window open on it [1][2]. All that code is
130+
trusted. What decides the architecture is how much trusted code there is, who
131+
writes it, and how a change to it gets reviewed. The host bundle is ruled out
132+
outright: it evaluates card code.
133+
134+
Plain JavaScript keeps the trusted code small: about 100 KB of commented
135+
source, all of it written in this repository. It also costs a copy of every
136+
screen. `shell.css`, `credentials.css` and `dom.js` copy the host's auth
137+
screens, and tests compare the copies pixel by pixel to keep them in step [3].
138+
Hand-built DOM is manageable for a few forms. It isn't for the screens the
139+
secure window is gaining, such as key management, which need the state,
140+
composition and templates the rest of the team writes in Ember.
141+
142+
An Ember app of the pages' own has neither cost. A minimal Ember application
143+
(router, resolver and one component) is 293 KB of minified script, 91 KB
144+
gzipped, in a production build, and runs under the policy with no violation.
145+
That runtime is a fixed body of code from a widely used, pinned release, and
146+
it is the stack the team already writes and reviews. It needs no
147+
`unsafe-eval` once templates are compiled at build time. Components written
148+
for the package can follow the policy from the start. The import allowlist
149+
(§1.3) and the build record (§1.8) keep the trusted code to what the package
150+
names, and make each change to it visible.
151+
152+
The security logic stays as plain modules in one directory (§1.6), because
153+
that code is what a reviewer of the origin has to read closely. Plain modules
154+
keep it readable without knowing the framework, and testable on its own.
155+
156+
## 3. Considered options
157+
158+
- **The package's own Ember app (chosen).**
159+
- **The host app bundle.** It evaluates card code.
160+
- **An Ember app on boxel-ui's components.** A sign-in form built from them
161+
shipped 321 KB minified, and 1,265 KB through boxel-ui's index modules,
162+
since boxel-ui declares no `sideEffects`. LoadingIndicator and the
163+
BoxelIcon logo set `style` attributes the policy refuses. Every later
164+
boxel-ui change would also ship onto the origin.
165+
- **Plain JavaScript, in the realm server or in its own package.** It is the
166+
smallest trusted code, but it means a hand-copied second implementation of
167+
every screen, which grows with each screen the window gains.
168+
- **A small view library, such as Preact or Lit.** It is smaller than Ember,
169+
but it is a second UI stack for the team to learn and review.
170+
- **Relaxing the policy with `style-src-attr 'unsafe-inline'`.** It would
171+
also let any style attribute that injected markup carries take effect.
172+
173+
## 4. Consequences
174+
175+
Until the package exists, a new secure-origin screen waits for it, since
176+
adding one to the plain pages would add a gap. That orders new secure-window
177+
screens, such as key management, after the package scaffold. Until the host
178+
renders its screens from the package (§1.9), a change to the host's sign-in
179+
screens still needs the same change in the package.
180+
181+
## 5. Sources
182+
183+
1. `.claude/skills/secure-origin/SKILL.md`, "The governing fact".
184+
2. [ADR 005](005-authorize-key-changes-only-with-a-session-card-code-cant-reach.md)
185+
§1.4, the secure origin's pages and their policy.
186+
3. #6861, which adds the registration and password-reset pages, the copied
187+
styles and icons, and the pixel comparison.
188+
189+
## 6. Remediation
190+
191+
- `packages/realm-server/secure-origin/index.html`, `shell.js`, `shell.css`,
192+
`sign-in.js`, `sign-in-copy.js`, `register.js`, `password-reset.js`,
193+
`password-fields.js` and `dom.js` break §1.1. They are served as written,
194+
not built from `packages/secure-origin`.
195+
- `packages/realm-server/secure-origin/credentials.html` and
196+
`credentials.css` break §1.1, for the same reason.
197+
- `packages/realm-server/secure-origin/credentials.js`, `matrix.js`,
198+
`session.js`, `redact.js` and `app-origins.js` break §1.1 and §1.6. They
199+
are served as written, and they are not in `packages/secure-origin/src/security/`.
200+
- `packages/realm-server/lib/secure-origin.ts` (`SHELL_ASSETS`,
201+
`CREDENTIALS_ASSETS`) breaks §1.1 and §1.7. It serves fixed source files,
202+
not the package's build output read through a manifest.

‎docs/adr/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ or backfill one.
1818
| [003](003-reach-cloud-services-through-adapters.md) | Reach proprietary cloud services only through adapters | proposed |
1919
| [005](005-authorize-key-changes-only-with-a-session-card-code-cant-reach.md) | Authorize changes to a user's keys only with a session that card code can't reach | proposed |
2020
| [007](007-keep-motion-a-reactive-layer-that-state-never-waits-on.md) | Keep motion a reactive layer that application state never waits on | proposed |
21+
| [008](008-build-the-secure-origins-pages-as-their-own-ember-app.md) | Build the secure origin's pages as their own Ember app | proposed |

0 commit comments

Comments
 (0)