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
8 changes: 6 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jobs:
working-directory: pod-name-guard
run: npm ci && npm test

- name: account-admin tests
working-directory: account-admin
run: npm ci && npm test

- name: mcp-connector tests
working-directory: mcp-connector
run: npm ci && npm test
Expand Down Expand Up @@ -72,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 }
env: { POD_LAYOUT: subdomain, TRY_NETWORK: host, CHROMIUM_PATH: /usr/bin/google-chrome, ADMIN_EMAILS: smoke-admin@example.org }
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 }
env: { POD_LAYOUT: suffix, TRY_NETWORK: host, CHROMIUM_PATH: /usr/bin/google-chrome, ADMIN_EMAILS: smoke-admin@example.org }
run: |
test/try-local.sh 3124
node test/smoke.mjs http://localhost:3124 test/shots/suffix
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/stock-watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
gh issue create --title "$title" --body "$(printf '%s\n' \
"1. \`docker pull solidproject/community-server:$latest\`" \
"2. \`scripts/check-overrides.sh solidproject/community-server:$latest\`: merge every template it flags into overlay/, then \`--rebaseline\`." \
"3. \`ARG CSS_VERSION=$latest\` in the Dockerfile, \`@solid/community-server\` in pod-name-guard/package.json; rebuild, smoke both layouts." \
"3. \`ARG CSS_VERSION=$latest\` in the Dockerfile, \`@solid/community-server\` in pod-name-guard/package.json and account-admin/package.json; rebuild, smoke both layouts." \
"4. CHANGELOG \`## $latest-1\`, tag \`v$latest-1\`.")"

# Ahead of a release: the overlay built on stock's unreleased images, with
Expand Down 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, IMAGE: "solid-css-overlay:${{ matrix.tag }}" }
env: { POD_LAYOUT: subdomain, TRY_NETWORK: host, ADMIN_EMAILS: smoke-admin@example.org, 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, IMAGE: "solid-css-overlay:${{ matrix.tag }}" }
env: { POD_LAYOUT: suffix, TRY_NETWORK: host, ADMIN_EMAILS: smoke-admin@example.org, 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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@ 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

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.
- **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:
`"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.

## 7.3.0-3 — 2026-11-03

Overlay only, on stock CSS 7.3.0. An instance updates the image; nothing to
Expand Down
13 changes: 7 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ pins a version and mounts its own `overlay/`. **A feature every instance will
get is built here**, not in an instance.

CSS is never patched. The image adds files where stock CSS looks for them
(`templates/`), one config, and one component (`pod-name-guard/`). Adding a
second component is a decision to write down (an ADR in solid-kit), not a
commit: every component is code to carry across every CSS release.
(`templates/`), one config, and two components: `pod-name-guard/` and
`account-admin/` (solid-kit ADR 012). Adding another is a decision to write
down (an ADR in solid-kit), not a commit: every component is code to carry
across every CSS release, and every CSS bump checks each of them.

A collective's procedures and the `collectif` skill live beside the
connector (`mcp-connector/procedures/`, `mcp-connector/skills/collectif/`):
Expand All @@ -25,15 +26,15 @@ implement is solid-kit's ADR 006.
The comments explaining *what went wrong before* are the most valuable content
in these files. Keep them when changing the code around them.

Planned features (account deletion, the right to erasure, ...) and their
Planned features (the account manager page, the right to erasure, ...) and their
agreed shape: `ROADMAP.md`. What CSS itself does and does not do with
accounts: README, "Accounts and pods".

## Commands

See README, "Working on the image". The gate before a tag: both smoke runs
green and their screenshots looked at, `pod-name-guard` and `mcp-connector`
tests, `scripts/check-overrides.sh`.
green and their screenshots looked at, `pod-name-guard`, `account-admin` and
`mcp-connector` tests, `scripts/check-overrides.sh`.

## Rules that are easy to break

Expand Down
27 changes: 22 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
# a new overlay on the same stock is -n+1. See CHANGELOG.md.
#
# Nothing in CSS is patched. The image only adds files where stock CSS already
# looks for them (templates/), one config, and one component (PodNameGuard).
# looks for them (templates/), one config, and two components (PodNameGuard,
# and AdminDeleteAccountHandler: solid-kit ADR 012).
ARG CSS_VERSION=7.3.0

# PodNameGuard, built and tested in the stock image it will run in, so on the
Expand All @@ -26,16 +27,31 @@ COPY pod-name-guard/src ./src
COPY pod-name-guard/test ./test
RUN npm test

# account-admin, the second component (solid-kit ADR 012): built and tested
# the same way, in the same stock image.
FROM solidproject/community-server:${CSS_VERSION} AS admin
WORKDIR /admin
COPY account-admin/package.json account-admin/package-lock.json ./
RUN --mount=type=secret,id=npm_ca,required=false \
if [ -f /run/secrets/npm_ca ]; then export NODE_EXTRA_CA_CERTS=/run/secrets/npm_ca; fi; \
npm ci --no-audit --no-fund
COPY account-admin/tsconfig.json account-admin/.componentsignore ./
COPY account-admin/src ./src
COPY account-admin/test ./test
RUN npm test

FROM solidproject/community-server:${CSS_VERSION}
WORKDIR /community-server

# The guard goes where Components.js discovers modules (node_modules under the
# The components go where Components.js discovers modules (node_modules under the
# main module path). At runtime it requires @solid/community-server, which in
# this image is /community-server itself and not a package under node_modules,
# so NODE_PATH points at a folder holding just that name. Not a symlink inside
# node_modules: discovery would then find the CSS module twice.
COPY --from=guard /guard/package.json node_modules/@solid-css-overlay/pod-name-guard/package.json
COPY --from=guard /guard/dist node_modules/@solid-css-overlay/pod-name-guard/dist
COPY --from=admin /admin/package.json node_modules/@solid-css-overlay/account-admin/package.json
COPY --from=admin /admin/dist node_modules/@solid-css-overlay/account-admin/dist
RUN mkdir -p /opt/css-resolve/@solid && ln -s /community-server /opt/css-resolve/@solid/community-server
ENV NODE_PATH=/opt/css-resolve

Expand All @@ -59,11 +75,12 @@ COPY config/ /config/
ENV CSS_CONFIG=/config/instance.json

# What this instance offers (/.well-known/css/provider.json), its pod layout
# for the pages and its email sender (config/identity/email/overlay.json, which
# both configs import), written at every start from the environment, then the stock
# 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
# 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/welcome.js start/start.sh /opt/overlay/
COPY start/describe.js start/email.js start/admins.js start/welcome.js start/start.sh /opt/overlay/
ENTRYPOINT ["/opt/overlay/start.sh"]
# CSS_BASE_URL is the instance's (compose .env); CSS_ROOT_FILE_PATH=/data is stock.
39 changes: 31 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ 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"). |
| **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. |
| **CSS itself** | Untouched. Files are added where stock CSS already looks (`templates/`), plus one config and one component. |
| **CSS itself** | Untouched. Files are added where stock CSS already looks (`templates/`), plus one config and two components (solid-kit ADR 012 says why the second). |

`config/suffix.json` keeps pods as folders (`https://<server>/<name>/`), for an
instance that already has pods that way. **An instance never switches between
Expand Down Expand Up @@ -313,16 +314,35 @@ by itself (`BaseLoginAccountStorage`, 1800 s). It is the only deletion CSS does.
- a WebID link;
- a pod owner.

Nothing deletes the account or the pod, so a pod name, once taken, stays
taken, and the right to erasure cannot be honoured yet. Both are planned
(`ROADMAP.md`).
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:
```sh
ADMIN_EMAILS=you@example.org # comma-separated; empty: nobody
```
**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`).

**Resetting a test instance** deletes every account and pod at once, in
`instance-template/`:
```sh
docker compose down -v && docker compose up -d
```
It removes the `<INSTANCE_NAME>_css-data` volume and the connector's audit
Still the quickest way to start a test instance over; one account at a time
is `scripts/admin.sh` above. It removes the `<INSTANCE_NAME>_css-data` volume and the connector's audit
volume. The welcome page is rewritten on the next first start. Put
`mcp-connector/identities.json` back to `{}` too: its agents' accounts are
gone. **Never on an instance with real accounts.**
Expand All @@ -337,6 +357,7 @@ node test/smoke.mjs http://localhost:3123 test/shots/subdomain
POD_LAYOUT=suffix test/try-local.sh 3124 # folder pods
POD_LAYOUT=suffix node test/smoke.mjs http://localhost:3124 test/shots/suffix
(cd pod-name-guard && npm ci && npm test)
(cd account-admin && npm ci && npm test)
(cd mcp-connector && npm ci && npm test)
scripts/check-overrides.sh # stock templates we replace, unchanged?
```
Expand All @@ -355,14 +376,16 @@ The skin audit (contrast, complete slot set) lives in solid-kit:
| `config/instance.json`, `config/suffix.json` | the two configs; the comments say why each line differs from stock |
| `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/` | the one component: pod names that make valid hostnames |
| `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/` |
| `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 and the welcome page, then runs stock CSS |
| `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`) |
| `ROADMAP.md` | planned features: account deletion, the right to erasure |
| `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 |
54 changes: 24 additions & 30 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,37 @@ Features every instance will get, not built yet. Each one says why it is
needed and the shape agreed so far. What CSS itself does and does not do
today: README, "Accounts and pods".

## Account and pod deletion, for admins (next)

**Why.** CSS 7.2 deletes no account and no pod. Test rounds leave accounts
that cannot be removed, and a pod name once taken stays taken. pocpod0
collected many throwaway accounts this way. Until this exists, a test
instance is cleaned by a full reset (README), which only works while no real
account lives there.

**Shape.**
- **Inside CSS, as a component on an admin route under `/.account/`.**
- It uses CSS's own stores and locks.
- It deletes the pods' resources depth first.
- Then it calls `delete('account', id)`, which removes the account's
logins, pods, WebID links and client credentials in one go.
- Deleting files from the volume from outside is not an option: that
storage format is CSS's private business and can change at any release,
and a script would race the server's locks.
- **Admins are login emails listed in the instance's config.** The config
is not edited by an instance, so the list comes from its environment and
is written at start, as the email sender is (`start/email.js`, 7.2.0-9).
- **Front doors**, all calling that one route:
1. `scripts/admin.sh` (list, delete-account), with a `make` target on the
VPS;
2. an **account-manager page** in the overlay: accounts and their pods,
each with a delete button.
- **Outside CSS**: the connector's `identities.json` entries for a deleted
WebID are removed as well.
- **It is the image's second component**, so a solid-kit ADR comes first
(CLAUDE.md): every component is code to carry across every CSS release.
## 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.

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.

**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).

## 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
route as above, limited to their own account. The page says plainly what goes:
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,
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;
- the agents' access.
Expand Down
7 changes: 7 additions & 0 deletions account-admin/.componentsignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"Omit",
"Dict",
"NodeJS.Dict",
"IndexTypeCollection",
"Promise"
]
2 changes: 2 additions & 0 deletions account-admin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
Loading
Loading