Skip to content

Commit 2e7379c

Browse files
marc0oloclaude
andcommitted
docs: add sync frontmatter and drop em-dashes from docs/
Makes docs/ syncable into developer-docs as the Frontends section, with this repo staying the single source of truth (#124). - Add Starlight frontmatter (title, description, sidebar.order) to all eight pages, and drop the H1 now that the frontmatter carries the title. GitHub renders the frontmatter as a table, nested sidebar.order included, so the title is still visible here; keeping both would mean five of the eight pages carrying a title that disagrees with their H1. This also makes the sync a pure rsync, as caffeinelabs/motoko does. No anchor moves: every cross-page link targets an H2 or H3. - Replace all 124 em-dashes. developer-docs bans U+2014 repo-wide; doing it here rather than in their postprocess keeps the published page and the file in this repo byte-identical, which is what makes the sync trustworthy. Definition bullets take the `- **Term.** Sentence.` form already used in routing.md; prose takes a colon, semicolon, comma, or parentheses per sentence. - Point the custom-domains link at the live URL. The old building-apps/frontends/... form is the retired portal layout and only survives on a redirect. - Make the three links that escape docs/ (ARCHITECTURE.md, README.md#releasing, examples/spa) absolute GitHub URLs, so no link rewriting is needed on the sync side. - Fix the stale example version in overview.md: v1.0.0 -> v0.3.3. The `<version>` placeholder convention in overview.md is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ac2a8e7 commit 2e7379c

8 files changed

Lines changed: 194 additions & 153 deletions

File tree

docs/access-protection.md

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
# Access protection (private apps)
1+
---
2+
title: "Access protection"
3+
description: "Put a login page in front of a private or preview site with revocable, expiring access tokens"
4+
sidebar:
5+
order: 6
6+
---
27

38
By default every deployed app is public. **Access protection** puts a login screen
49
in front of it: unauthenticated visitors get a certified redirect to your login
510
page (or a `401` for non-page assets) instead of your content. It's meant to
6-
**deter casual/public access** to in-progress or preview work not to provide
11+
**deter casual/public access** to in-progress or preview work, not to provide
712
confidentiality against a determined attacker (see [Threat model](#threat-model)).
813

914
Access is a set of **labeled tokens**. The cookie *is* the token: a visitor presents
1015
a token, the canister sets it as a cookie, and every request re-validates that cookie
1116
against the token store. A classic single "password" is just one long-lived token.
1217

13-
When protection is off, the app is **completely unchanged** no gate, no extra
18+
When protection is off, the app is **completely unchanged**: no gate, no extra
1419
headers, no cost difference. Everything below applies only once you enable it.
1520

1621
## Quick start
@@ -30,7 +35,7 @@ icp canister call frontend issue_token \
3035
'(record { label = "owner"; ttl_secs = 31536000 : nat32; value = opt "my-passphrase" })'
3136
```
3237

33-
That's it `https://<canister-id>.icp0.io/` now redirects strangers to
38+
That's it: `https://<canister-id>.icp0.io/` now redirects strangers to
3439
`/login.html`, and visitors who present `my-passphrase` get in.
3540

3641
> **Enable before the first sync for a brand-new private app.** Enabling on an empty
@@ -42,29 +47,29 @@ That's it — `https://<canister-id>.icp0.io/` now redirects strangers to
4247
| Method | Call | Effect |
4348
|---|---|---|
4449
| Issue | `issue_token '(record { label = "<label>"; ttl_secs = <secs> : nat32; value = opt "<value>" })'` | Mints a token and **returns its value**. Pass `value = null` (or leave the field out) to get a high-entropy random token instead of a chosen passphrase. |
45-
| Revoke | `revoke_token '("<label>")'` | Removes every token with that label **live**, so the next request bearing it is rejected. |
50+
| Revoke | `revoke_token '("<label>")'` | Removes every token with that label. Takes effect **live**, so the next request bearing it is rejected. |
4651
| List | `list_tokens '()'` | Returns `{ label; expires_at }` for every live token (controller-only). |
4752
| Status | `check_protection_status '()'` | `Disabled`, `Enabled`, or `EnabledLoginPageMissing` (controller-only). |
4853
| Disable | `disable_protection '()'` | Turns the gate off and drops all tokens. |
4954

50-
Always pass the argument explicitly `'()'` for the methods that take none. Given no
55+
Always pass the argument explicitly (`'()'` for the methods that take none). Given no
5156
argument, `icp canister call` opens an interactive prompt instead of sending an empty one.
5257

53-
Each token has its **own expiry** and is **individually revocable** revoking one
58+
Each token has its **own expiry** and is **individually revocable**: revoking one
5459
leaked share doesn't disturb anyone else. Expired tokens are rejected immediately
5560
whether or not they've been swept, and are garbage-collected as new ones are issued.
5661

5762
**Random vs. chosen value.** A random token (omit the chosen value) is unguessable
5863
and never transits the subnet in readable form. A chosen passphrase is typeable but
5964
brute-forceable, and is visible to node operators in the block that carries the
60-
`issue_token` callacceptable under the threat model, but prefer random tokens for
61-
share links.
65+
`issue_token` call. That is acceptable under the threat model, but prefer random
66+
tokens for share links.
6267

6368
## The login page
6469

6570
The login page is **your own asset**, synced in `dist/` and named when you
6671
`enable_protection`. The canister serves it certified like any other page; it is the
67-
**only gate-exempt path**, so it must be **fully self-contained** inline its CSS and
72+
**only gate-exempt path**, so it must be **fully self-contained**: inline its CSS and
6873
JS, and use `data:` URIs for images. Any external subresource it referenced would
6974
itself be gated and fail to load for a logged-out visitor.
7075

@@ -105,7 +110,7 @@ A minimal page that does both:
105110

106111
| Request | Response |
107112
|---|---|
108-
| An HTML page (or any path with no exact assetSPA routes, 404s) | Certified `307 → <login_page>` |
113+
| An HTML page (or any path with no exact asset, such as SPA routes and 404s) | Certified `307 → <login_page>` |
109114
| A non-HTML asset (JS/CSS/image/JSON) | Certified `401` (a redirect would hand a `<script>` the wrong content type) |
110115
| The login page itself | Always served (gate-exempt) |
111116

@@ -116,12 +121,12 @@ cookie-blind boundary cache can never replay one visitor's response to another.
116121

117122
`check_protection_status` reports one of:
118123

119-
- **`Disabled`** public app.
120-
- **`Enabled`** gate on, login page present.
121-
- **`EnabledLoginPageMissing`** gate on, but the named login-page asset isn't
124+
- **`Disabled`**: public app.
125+
- **`Enabled`**: gate on, login page present.
126+
- **`EnabledLoginPageMissing`**: gate on, but the named login-page asset isn't
122127
synced. The app **stays protected** (no content is served); requests redirect to a
123-
page that 404s until you sync it. It **self-heals** to `Enabled` once the page lands
124-
no need to re-enable.
128+
page that 404s until you sync it. It **self-heals** to `Enabled` once the page
129+
lands, with no need to re-enable.
125130

126131
The gate **fails closed**: a sync that removes the login page, or enabling before the
127132
first sync, never exposes content.
@@ -136,27 +141,27 @@ store, so a token replayed against another canister isn't in *its* store.
136141

137142
The canister serves **response-only** certified responses: the HTTP gateway (and any
138143
verifier) checks that each response is an *authentic, certified* response for the
139-
requested pathbut **not** which of several certified responses the canister chose to
140-
return. Choosing one is ordinary application logic.
144+
requested path, but **not** which of several certified responses the canister chose
145+
to return. Choosing one is ordinary application logic.
141146

142147
That is the same mechanism the canister already uses to serve more than one response
143-
per URL different content encodings, `200` vs `304`, `206` range responses, and the
148+
per URL: different content encodings, `200` vs `304`, `206` range responses, and the
144149
redirect/rewrite rules that serve one asset's body under another path. Access
145150
protection has the same shape: under every protected path the canister certifies
146-
**both** the asset's normal responses **and** a certified *unauthenticated sibling*
147-
the `307 → <login_page>` (HTML pages) or the `401` (other types). At serve time it
148-
reads the `certified_assets_access` cookie and returns the sibling when it's missing or
149-
invalid, the real asset when it's valideach one independently verified by the
151+
**both** the asset's normal responses **and** a certified *unauthenticated sibling*,
152+
either the `307 → <login_page>` (HTML pages) or the `401` (other types). At serve time
153+
it reads the `certified_assets_access` cookie and returns the sibling when it's missing
154+
or invalid, the real asset when it's valid; each one is independently verified by the
150155
gateway. The login page's path additionally carries the certified `302 + Set-Cookie`
151156
redeem (one per token) and the `401` re-prompt, so a login `POST` is honored only
152157
because its outcome is certified too.
153158

154159
**Why not certify the request?** Doing so would let the canister prove *which* request
155-
it answered, but request certification hashes the whole `Cookie` header verbatim — it
156-
can't match on a single *named* cookie. Nor would it fire: the canister sets its own
160+
it answered, but request certification hashes the whole `Cookie` header verbatim, so
161+
it can't match on a single *named* cookie. Nor would it fire: the canister sets its own
157162
`ic_env` cookie on HTML responses, so a browser always sends
158163
`Cookie: ic_env=…; certified_assets_access=…`, never a bare value. Picking one cookie
159-
out of many is necessarily app-side logic hence response-only.
164+
out of many is necessarily app-side logic, hence response-only.
160165

161166
**The caveat this creates.** Because the cookie→response choice is uncertified app
162167
logic, *which* response a request receives is **not** covered by the certificate. An
@@ -168,16 +173,16 @@ and it sets up the trust boundary the threat model below makes precise.
168173
## Threat model
169174

170175
This is **access gating, not confidentiality.** Under the IC's honest-replica /
171-
honest-boundary-node assumption the same assumption all query serving already makes
172-
unauthorized visitors can't pull your content. But:
176+
honest-boundary-node assumption (the same assumption all query serving already
177+
makes), unauthorized visitors can't pull your content. But:
173178

174179
- Asset bytes and the token store live in replicated canister state, so a **node
175180
operator can read both**. Protection does not hide content from operators. (Random
176181
token values are stored hashed; a low-entropy chosen passphrase is still
177182
brute-forceable, like any password hash.)
178183
- TLS terminates at the boundary node, which sees the cookie in clear.
179-
- There is **no brute-force protection, lockout, or rate-limiting** login attempts
184+
- There is **no brute-force protection, lockout, or rate-limiting**: login attempts
180185
are unbilled, untracked queries. The deterrent is high-entropy random tokens.
181186

182-
Use it to keep a preview or in-progress app out of public view not to protect
187+
Use it to keep a preview or in-progress app out of public view, not to protect
183188
secrets from a determined adversary.

docs/headers.md

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
# Custom headers
1+
---
2+
title: "Custom headers"
3+
description: "The _headers file: cache-control, security headers, content types, and how rules combine"
4+
sidebar:
5+
order: 4
6+
---
27

38
Add a file named `_headers` to the root of your asset directory to attach response
4-
headers cache-control, a Content Security Policy, other security headers to files
9+
headers (cache-control, a Content Security Policy, other security headers) to files
510
on your site. The syntax follows [Netlify's `_headers`](https://docs.netlify.com/manage/routing/headers/):
611
a path pattern on its own line, followed by indented `Name: value` lines.
712

@@ -20,28 +25,28 @@ A blank line or a `#` comment ends a block.
2025

2126
A pattern is an absolute path (leading `/`) with an optional `*` wildcard:
2227

23-
- `/index.html` one exact file (the home page).
24-
- `/about.html` likewise, one exact file.
25-
- `/assets/*` everything under `/assets/`.
26-
- `/*.css` every file ending in `.css`.
27-
- `/*` every file.
28+
- `/index.html`: one exact file (the home page).
29+
- `/about.html`: likewise, one exact file.
30+
- `/assets/*`: everything under `/assets/`.
31+
- `/*.css`: every file ending in `.css`.
32+
- `/*`: every file.
2833

2934
The single `*` matches any run of characters, including `/`. There is no `**`, no
3035
`?`, and no `:placeholder` capture (see [why dynamic rules aren't supported](redirects.md#what-isnt-supported-dynamic-rules)).
3136

3237
### Patterns match files, not URLs
3338

34-
A pattern is matched against the **path of the file inside your directory** its
35-
asset key not against the URL a visitor typed. For most sites the two are the same
39+
A pattern is matched against the **path of the file inside your directory** (its
40+
asset key), not against the URL a visitor typed. For most sites the two are the same
3641
string and the distinction never comes up, but it matters wherever a URL and a file
3742
differ:
3843

3944
- **Clean URLs.** Write `/about.html`, not `/about`, and `/index.html` for the home
4045
page. There is no file at `/`, so a `/` pattern matches nothing at all.
4146
- **Rewrites.** A `200` [rewrite](redirects.md) serves its target file's contents,
4247
and reuses that file's headers. So the headers for `/article` come from the pattern
43-
matching `/content/article.html`. For a [SPA](routing.md#single-page-apps-spa) where
44-
a single `/*` rewrite serves the shell at every client route this means a
48+
matching `/content/article.html`. For a [SPA](routing.md#single-page-apps-spa), where
49+
a single `/*` rewrite serves the shell at every client route, this means a
4550
`Cache-Control` declared for `/index.html` reaches all of them, and a block written
4651
against a route like `/dashboard/*` matches no file and does nothing.
4752
- **Redirects.** A `3xx` rule is the one exception: it synthesizes its own response
@@ -51,7 +56,7 @@ differ:
5156
## How rules combine
5257

5358
A file can match several blocks at once, and **all matching blocks contribute** their
54-
headers patterns don't override each other wholesale. For a single header name:
59+
headers; patterns don't override each other wholesale. For a single header name:
5560

5661
- Values from different matching blocks are combined into one header, comma-separated
5762
(per the HTTP spec).
@@ -76,7 +81,7 @@ Unlike other headers, `Content-Type` is single-valued and first-match-wins.
7681

7782
## No headers are added for you
7883

79-
The canister applies **no default headers** no `Cache-Control`, no security headers,
84+
The canister applies **no default headers**: no `Cache-Control`, no security headers,
8085
no CSP. If you want them, declare them in `_headers`. The only response headers the
8186
canister manages on its own are the ones tied to how it serves and certifies content:
8287
`Content-Type`, `Content-Encoding`, `ETag`, `Content-Range` (on large-asset
@@ -92,7 +97,7 @@ A useful baseline to copy and adapt:
9297
Referrer-Policy: strict-origin-when-cross-origin
9398
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'
9499
95-
# Fingerprinted build assets never change cache them hard
100+
# Fingerprinted build assets never change, so cache them hard
96101
/assets/*
97102
Cache-Control: public, max-age=31536000, immutable
98103
@@ -107,8 +112,8 @@ HTML responses, so don't reuse that name.
107112
## Reserved headers
108113

109114
Some headers can't be set in `_headers`. The canister either computes them itself or
110-
they have no meaning for a certified asset served through the IC HTTP gateway — a
111-
wrong value would corrupt the response or simply be unverifiable. Rather than silently
115+
they have no meaning for a certified asset served through the IC HTTP gateway, where
116+
a wrong value would corrupt the response or simply be unverifiable. Rather than silently
112117
ignore these (as some CDNs do, which leads to confusing bugs), the sync plugin
113118
**rejects them at deploy time** with an explanatory error, so you find out
114119
immediately.
@@ -124,5 +129,5 @@ immediately.
124129
| `IC-Certificate`, `IC-CertificateExpression` | These carry the response certificate and are canister-managed. |
125130
| `Location` | A `Location` here wouldn't redirect (the status stays `200`). Use [`_redirects`](redirects.md) instead. |
126131

127-
This list is intentionally conservative and may be relaxed in future releases it's
132+
This list is intentionally conservative and may be relaxed in future releases; it's
128133
easier to allow a header later than to start rejecting one that sites already rely on.

0 commit comments

Comments
 (0)