Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ jobs:
run: npm ci && /usr/bin/google-chrome --version

- name: smoke, subdomain pods (the image's default)
env: { POD_LAYOUT: subdomain, TRY_NETWORK: host, CHROMIUM_PATH: /usr/bin/google-chrome, ADMIN_EMAILS: smoke-admin@example.org }
env: { POD_LAYOUT: subdomain, TRY_NETWORK: host, CHROMIUM_PATH: /usr/bin/google-chrome }
run: |
test/try-local.sh 3123
node test/smoke.mjs http://localhost:3123 test/shots/subdomain
test/welcome-restart.sh 3123
docker rm -f try-css-overlay-3123

- name: smoke, folder pods (config/suffix.json)
env: { POD_LAYOUT: suffix, TRY_NETWORK: host, CHROMIUM_PATH: /usr/bin/google-chrome, ADMIN_EMAILS: smoke-admin@example.org }
env: { POD_LAYOUT: suffix, TRY_NETWORK: host, CHROMIUM_PATH: /usr/bin/google-chrome }
run: |
test/try-local.sh 3124
node test/smoke.mjs http://localhost:3124 test/shots/suffix
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/stock-watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ jobs:
id: smoke_subdomain
if: steps.build.outcome == 'success'
continue-on-error: true
env: { POD_LAYOUT: subdomain, TRY_NETWORK: host, ADMIN_EMAILS: smoke-admin@example.org, IMAGE: "solid-css-overlay:${{ matrix.tag }}" }
env: { POD_LAYOUT: subdomain, TRY_NETWORK: host, IMAGE: "solid-css-overlay:${{ matrix.tag }}" }
run: |
test/try-local.sh 3123
node test/smoke.mjs http://localhost:3123 "test/shots/$TAG-subdomain"
- name: smoke, folder pods
id: smoke_suffix
if: steps.build.outcome == 'success'
continue-on-error: true
env: { POD_LAYOUT: suffix, TRY_NETWORK: host, ADMIN_EMAILS: smoke-admin@example.org, IMAGE: "solid-css-overlay:${{ matrix.tag }}" }
env: { POD_LAYOUT: suffix, TRY_NETWORK: host, IMAGE: "solid-css-overlay:${{ matrix.tag }}" }
run: |
docker rm -f try-css-overlay-3123 >/dev/null 2>&1 || true
test/try-local.sh 3124
Expand Down
57 changes: 30 additions & 27 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,41 @@ Versions are `<stock CSS version>-<n>`: a new stock release starts at `-1`, a
new overlay on the same stock adds one. A tag `v<version>` publishes both
images (`.github/workflows/build.yml`).

## 7.3.0-4 — 2026-11-03
## 7.3.0-4 — unreleased

On stock CSS 7.3.0. **The image's second component** (solid-kit ADR 012). An
instance updates the image; to use it, add `ADMIN_EMAILS` to `.env`
(`instance-template/.env.example`) and the line that passes it to
`compose.yml` comes with the checkout.

- **Admins can delete an account and its pods** (#17, slice 1). Stock CSS
deletes neither, so a pod name once taken stayed taken and test rounds could
only be cleaned by wiping the volume. `account-admin/` adds
`POST /.account/admin/delete-account/ { "email" }`: the pods' contents are
deleted through the resource store, each pod root through the data accessor
under the write lock (the store refuses a root storage, and a root left
behind keeps the name taken), then the account record with its logins,
WebID links and client credentials. Only accounts whose login is in
`ADMIN_EMAILS` may call it (`start/admins.js` writes it at start, like the
email sender); empty, it refuses everyone. An admin cannot delete their own
account there.
- **`scripts/admin.sh delete-account <email>`**: signs in, asks for the email
again, calls the route, then removes the connector's agents for that
account from `identities.json` (new `removeIdentitiesFor` in
`mcp-connector/src/identityRegistry.js`).
- Smoke (both layouts, `ADMIN_EMAILS` set in CI): a throwaway account with a
pod is refused to a non-admin and to nobody signed in, deleted by the admin,
its login no longer works, and its pod name is taken again.
instance updates the image; nothing to add to `.env`.

- **List every account and delete one with its pods, from the server's
terminal** (#17). Stock CSS does neither, so a pod name once taken stayed
taken and test rounds could only be cleaned by wiping the volume.
`scripts/admin.sh`, run from the instance's checkout, opens a TUI in the css
container (`admin/admin.js`): list, sort, filter, select, delete, with the
number of accounts typed to confirm. `list` and `delete … --yes` do the same
without the TUI. Docs: `docs/admin/` (tutorial, how-to, reference,
explanation).
- **How it deletes** (`account-admin/`, route `POST /.account/admin/accounts/`):
the pods' contents go through the resource store, each pod root through the
data accessor under the write lock (the store refuses a root storage, and a
root left behind keeps the name taken), then the account record with its
logins, WebID links and client credentials.
- **Who may: whoever runs a command on the server.** `start/admins.js` writes a
fresh random token at every start, readable by the container only; the tool
reads it there. The first draft listed admin login emails (`ADMIN_EMAILS`),
never released: CSS does not verify a login's email, so a listed address
nobody had used yet made whoever signed up with it first an admin.
- **The connector's agents** for a deleted account are removed from
`identities.json` by `scripts/admin.sh` (new `removeIdentitiesFor`).
- **Gateway templates** answer 404 for `/.account/admin/` from outside. An
existing gateway does not need it (the token protects the route), but add it
when you next touch the vhost.
- **An instance that mounts its own copy of `config/instance.json`** (more
reserved names, overlay-template) adds one line to its `import` list, after
the email one, or the route answers 404:
the email one, or the tool answers 404:
`"css:config/identity/handler/account-admin.json",`
- **List only emails that are already logins on the server**: stock CSS does
not verify a login's email, so an unclaimed address listed in
`ADMIN_EMAILS` makes whoever signs up with it first an admin.
- Smoke (both layouts): a throwaway account with a pod is listed by the tool
run inside the container and deleted by it. Its login no longer works, its
pod name is taken again, and a guessed token from outside is refused.

## 7.3.0-3 — 2026-11-03

Expand Down
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#
# Nothing in CSS is patched. The image only adds files where stock CSS already
# looks for them (templates/), one config, and two components (PodNameGuard,
# and AdminDeleteAccountHandler: solid-kit ADR 012).
# and AccountAdminHandler: solid-kit ADR 012) with its terminal tool (admin/).
ARG CSS_VERSION=7.3.0

# PodNameGuard, built and tested in the stock image it will run in, so on the
Expand Down Expand Up @@ -76,11 +76,14 @@ ENV CSS_CONFIG=/config/instance.json

# What this instance offers (/.well-known/css/provider.json), its pod layout
# for the pages, its email sender (config/identity/email/overlay.json) and its
# admin route (config/identity/handler/account-admin.json), both imported by
# both configs, written at every start from the environment, then the stock
# account admin route and its token (config/identity/handler/account-admin.json),
# both imported by both configs, written at every start from the environment, then the stock
# server. The version is the tag (the publish job passes it); local builds say dev.
ARG OVERLAY_VERSION=dev
ENV OVERLAY_VERSION=${OVERLAY_VERSION}
COPY start/describe.js start/email.js start/admins.js start/welcome.js start/start.sh /opt/overlay/
# The account admin's terminal tool, run with `docker compose exec css node
# /opt/overlay/admin.js` (scripts/admin.sh does it). Not an entrypoint step.
COPY admin/admin.js /opt/overlay/admin.js
ENTRYPOINT ["/opt/overlay/start.sh"]
# CSS_BASE_URL is the instance's (compose .env); CSS_ROOT_FILE_PATH=/data is stock.
31 changes: 11 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ there, only without this overlay's extras.
|---|---|
| **Pods are subdomains** | `https://<name>.<server host>/`, WebID `https://<name>.<server host>/profile/card#me`. The default for every new instance. |
| **Pod names are checked** | `PodNameGuard` (`pod-name-guard/`) refuses, with a 400 the form shows, a name that cannot be a hostname (`open_fab`, 64+ characters, a hyphen at either end) and reserved names (`www`, `mcp`, `api`, …: `config/instance.json`). CSS alone checks only that the name is not empty. The form runs the same check first. |
| **Admins can delete an account** | `AdminDeleteAccountHandler` (`account-admin/`): an account and its pods go, and the pod names are free again. Stock CSS deletes neither. Admins are the login emails in `ADMIN_EMAILS`; front door `scripts/admin.sh` (below, "Accounts and pods"). |
| **Accounts can be listed and deleted** | From the server's terminal: `scripts/admin.sh` opens a TUI that lists every account, then sorts, filters, selects and deletes them with their pods. The pod names come free again. Stock CSS does neither. [`docs/admin/`](docs/admin/README.md) |
| **Authentication is stock** | DPoP and Bearer only. pocpod0's `UnsecureWebIdExtractor` is not in the image. |
| **The welcome page** | Ours, or the vertical's from its design-system repo, put at the root at every start (`start/welcome.js`). |
| **The README a new pod starts with** | Ours: what is in the pod, what is public, how sharing and agents work, no Databrowser. Or the vertical's, from the same repo. |
Expand Down Expand Up @@ -317,24 +317,15 @@ by itself (`BaseLoginAccountStorage`, 1800 s). It is the only deletion CSS does.
Nothing in stock deletes the account or the pod, so a pod name, once taken,
stays taken.

**The image adds deletion, for admins** (since 7.3.0-4, solid-kit ADR 012).
List the admins' login emails in the instance's `.env`, then restart:
**The image adds listing and deletion, from the server's terminal** (since
7.3.0-4, solid-kit ADR 012). From the instance's checkout:
```sh
ADMIN_EMAILS=you@example.org # comma-separated; empty: nobody
scripts/admin.sh # TUI: list, sort, filter, select, delete
```
**Sign up with that email first.** Stock CSS does not verify a login's
email, so an address listed here that is not yet a login belongs to whoever
signs up with it first, and makes them admin.
From the instance's checkout:
```sh
scripts/admin.sh delete-account someone@example.org
```
It asks for your login, then the email again, and deletes: the account, its
pods and everything in them (the names are free again), its logins, WebID
links and client credentials, and the connector's agents that acted for it
(`identities.json`, when the connector runs there). An admin cannot delete
their own account this way. Self-service "Delete my account", the right to
erasure, comes next (`ROADMAP.md`).
Being able to run it is the permission (ssh and docker): there is no admin
password and no admin list. Tutorial, how-to, reference and explanation are in
[`docs/admin/`](docs/admin/README.md). Self-service "Delete my account", the
right to erasure, comes next (`ROADMAP.md`).

**Resetting a test instance** deletes every account and pod at once, in
`instance-template/`:
Expand Down Expand Up @@ -377,15 +368,15 @@ The skin audit (contrast, complete slot set) lives in solid-kit:
| `overlay/` | the pages: `main.html.ejs`, `main.css`, `pod-ui.js`, `pod-layout.js`, `templates/identity/**`, `templates/pod/base/README$.md.hbs` (a new pod's README), `welcome/`, `doc-view/` (generated from solid-kit, `SOURCE`) |
| `skins/pod/` | the default skin |
| `pod-name-guard/` | component 1: pod names that make valid hostnames |
| `account-admin/` | component 2: admins delete an account and its pods (solid-kit ADR 012) |
| `scripts/admin.sh` | the admin's front door to `account-admin/` |
| `account-admin/` | component 2: list accounts, delete one with its pods (solid-kit ADR 012) |
| `admin/admin.js`, `scripts/admin.sh` | the account admin's terminal tool, and its launcher on an instance |
| `upstream-baseline.sha256`, `scripts/check-overrides.sh` | the stock templates `overlay/` replaces, hashed |
| `test/` | `try-local.sh` (runs the image) + `smoke.mjs` (a real browser, both layouts) |
| `instance-template/` | what an instance runs from (its checkout): compose, `.env.example`, `email.env.example`, `update.sh`, the Caddy and nginx gateway templates |
| `overlay-template/` | what the `pods/` folder of a vertical's design-system repo starts from (ADR 011) |
| `start/` | the image's entrypoint: writes `provider.json`, `pod-layout.js`, the email sender, the admin route and the welcome page, then runs stock CSS |
| `gateway-caddy/` | Caddy + the OVH DNS module, for the wildcard certificate |
| `docs/` | `faq.md` (sign-in lifetimes, consent), install briefs for one VPS (`handoff-mapsofmaking.md`, `handoff-pocpod0.md`) |
| `docs/` | `admin/` (account admin: tutorial, how-to, reference, explanation), `faq.md` (sign-in lifetimes, consent), install briefs for one VPS (`handoff-mapsofmaking.md`, `handoff-pocpod0.md`) |
| `ROADMAP.md` | planned features: the account manager page, the right to erasure |
| `mcp-connector/` | the connector's source and image |
| `skills/css-instance/` | the method, for Claude Code; never required |
28 changes: 12 additions & 16 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,30 @@ today: README, "Accounts and pods".

## Account and pod deletion, for admins

**Slice 1 done (7.3.0-4, solid-kit ADR 012):** `account-admin/`, route
`POST /.account/admin/delete-account/`, admins from `ADMIN_EMAILS`
(`start/admins.js`), front door `scripts/admin.sh delete-account`, which
also drops the connector's agents for that account. The smoke run deletes an
account with a pod and takes the same name again, on both layouts.
**Done (7.3.0-4, solid-kit ADR 012 and its amendment):** `scripts/admin.sh`,
a TUI run from the server's terminal that lists, sorts, filters, selects and
deletes accounts with their pods, and removes the connector's agents for them.
The permission is shell access on the server (a token the container renews at
every start), not a list of admin emails. Docs: `docs/admin/`.

What was learnt building it: the store refuses to delete a pod root (a root
storage, Solid §5.4) and CSS refuses to create a pod while anything is at its
base, so the root goes through the data accessor under the write lock
(`AdminDeleteAccountHandler.ts`, header). One more CSS internal to check at
each bump.
(`account-admin/src/AccountAdminHandler.ts`, header). One more CSS internal to
check at each bump.

**Next, slice 2: see and choose.**
- `scripts/admin.sh list`: accounts, their logins and pods. Needs the account
storage's `entries('account')`; the stores have no "list all".
- An **account-manager page** in the overlay for admins: accounts and their
pods, a delete button each, calling the same route.
- A `make` target on the VPS (vps-infra, not this repo: an instance's
checkout is never edited).
**Left:** a `make` target on the VPS (vps-infra, not this repo: an instance's
checkout is never edited). A browser page for admins is not planned: it would
need its own answer to "who is this?" (ADR 012).

## Self-service erasure: the right to erasure

**Why.** A person must be able to have their data erased (GDPR art. 17). On a
CSS instance today, nobody can do it, not even an admin.

**Shape.** "Delete my account" on the person's own account page, on the same
handler as above, limited to their own account (the admin route refuses an
admin's own account on purpose: that is this door). Where the button lives,
deletion as above, limited to their own account and opened by their own
sign-in instead of the server's token. Where the button lives,
the overlay or solid-backoffice, is ADR 012's open question. The page says plainly what goes:
- their pods and everything in them;
- their WebID;
Expand Down
Loading
Loading