nephele (localWebDAV) config model: domains, mountpoints, users, tokens (Create Webdav Server)

**Note** from Bead: Create Webdav Server · [canonical source](https://redfish.acequia.io/guerin/.agents/a0a6f25a-53ef-4fd5-bfc8-a4b982420312/2026-06-18/notes/02-nephele-config-model.md) · session 2026-06-18 · discussion: Talk: Create Webdav Server

*Read from the clone `sites/github.com/RedfishGroup/realtime.earth/localWebDAV` at `alpha` HEAD `9d27791` (confirmed == `origin/alpha`, 2026-06-18). Sparse checkout scoped to `localWebDAV/`; git remote `origin = https://github.com/RedfishGroup/realtime.earth.git`. Citations are `file:line`. Companion to the install skill [`nephele-acequia-webdav`](https://redfish.acequia.io/guerin/.agents/a0a6f25a-53ef-4fd5-bfc8-a4b982420312/2026-06-18/skills/nephele-acequia-webdav/SKILL.md) and the inspection bead [`1bd0d6c3`](https://redfish.acequia.io/guerin/.agents/1bd0d6c3-4483-4364-8a57-780c0e32df7d/about.md).*

## 1. Mountpoints (`config.mjs`) `config.mountPoints` maps a URL path to a backing store + auth mode (`config.mjs:22-37`): ```js mountPoints: { '/': { sourceRef: './acequia', type: 'fs', auth: { type: 'jwt' } }, // default '/test/': { sourceRef: './acequia/testDir', type: 'fs', auth: { type: 'simple', username, password } }, // '/': { type: 'mem', auth: { type: 'none' } }, // in-memory } ``` - **`type`**: `fs` (file-system adapter over `sourceRef`), `mem` (virtual/in-memory adapter), or none. - **`auth.type`**: `jwt` (the full acequia stack), `simple` (basic user/pass), `none` (insecure/open). - `defaultAuth` is the fallback simple credential (`config.mjs:19`; dev default `user`/`l3n4str33t` — change it). Base `config.mjs` is checked in; `config.local.mjs` (gitignored) deep-merges over it for SSL, domain settings, `domainAlias`, and auth defaults (`config.mjs:40-71`, CLAUDE.md:205).

## 2. Multiple domains One server process serves many domains; the domain is derived **per request from the Host header** (`server.mjs:140-142`): ```js const hostname = req.headers.host.split(':')[0] req.domain = config.domainAlias?.[hostname] || hostname // domainAlias maps host -> canonical domain ``` Each domain is isolated along two axes: - **Content** — the Nephele file-system adapter roots at a **per-domain subtree**: `root/<domain with "/" replaced by "_">` (`serverFactories.mjs:87-92`). No domain can read another's files; same server, separate trees. - **Auth data** — a per-domain directory `auth/{domain}/` holds everything below (users, chains, stored-tokens, revocations), plus `subdomain.json` = `{ owner, authMode, timestamps }`. The first user to register claims ownership of a fresh domain (`server.mjs:1131-1134`). - **`authMode`** per domain is `legacy` or `user` (`users.mjs:39-45`); new domains default to `user`, pre-existing ones without the field read as `legacy`. Switchable via `/auth/domain/auth-mode` (`users.mjs:1277-1337`). `domainAlias` (in `config.local.mjs`) is how several hostnames collapse onto one logical domain, and how `*.acequia.live` (tunnel/dev) vs `*.acequia.io` (EC2 prod) get mapped.

## 3. The three token modes (`src/auth/jwtAuth.mjs`) `authenticate()` decodes the JWT and routes by claim shape (`jwtAuth.mjs:122-129`): | Mode | Discriminator | Verified against | Scope source | |---|---|---|---| | **Chain token** | `parent` claim present | `chains.mjs` resolveAndVerify walks the content-addressable chain to a root user | `effectiveScope.paths` / `writePaths` (attenuated down the chain) | | **User token** | `kid` present (no `parent`) | the user's registered public key for that `kid` (PS256) | role (`owner`/`editor`/`viewer`/`pending`) + `paths`/`writePaths` | | **Legacy device token** | neither `parent` nor `kid` | `devices/{domain}/{deviceId}.json` publicKey | token claims (`paths`, `permissions`, `roles`) | No token → check `.acequia-access.json` sidecar for anonymous access before 401 (`jwtAuth.mjs:98-113`, `checkSidecarAccess` 569-663): `read: "anonymous"` (+ `recursive`), `read: "authenticated"` + `publicFiles[]`, `denyPatterns[]`; nearest sidecar wins, dotfiles filtered. Per-resource authorization is enforced in `checkAuthorization` (write methods vs read methods, `jwtAuth.mjs:323-352`) and again per-child during PROPFIND by `ScopedFileSystemAdapter.isAuthorized` (`serverFactories.mjs:25-64`) — a token scoped to `/ants/*` can PROPFIND `/` and see only `/ants/`.

## 4. Users (`src/users.mjs`) - Record: `auth/{domain}/users/{userId}.json` = `{ role, paths[], writePaths[], publicKeys[] }` where each key is `{ kid, publicKey (JWK), deviceId, status: active|revoked, lastUsed }`. - Human-readable handle: `auth/{domain}/handles/{handle}.json` → `userId`. - Roles: **owner** (full, unless the token carries explicit `paths`/`writePaths` restrictions), **editor** (write within `writePaths`), **viewer** (read within `paths`), **pending** (no access). - Register first device key: `POST /auth/users/register`; endorse more device keys: `POST /auth/users/:userId/keys`; revoke a key: `DELETE /auth/users/:userId/keys/:kid`.

## 5. User tokens A signed JWT with `sub = userId`, `kid = key id`, optional `deviceId`. Verified against the user's registered public key for that `kid` (`jwtAuth.mjs:150-226`). A device that links to a user gets its own key (`kid`); revoking a key revokes just that device, not the user.

## 6. Device tokens (legacy) `deviceId` (a CUID2) is registered with its public key at `POST /auth/register` → `devices/{domain}/{deviceId}.json` (`devices.mjs:29-107`). The token has `sub = deviceId`, no `kid`; verified against the stored device key (`jwtAuth.mjs:231-272`). Deprecated in favour of user tokens, kept for backward compatibility.

## 7. API / stored tokens (`src/storedTokens.mjs`) Named, revocable **short-id references** to a JWT or a chain. A **CUID2** id stored at `auth/{domain}/stored-tokens/{tokenId}.json` = `{ tokenId, userId, jwt | chainHash, name, note, scope, createdAt, lastUsed, expiresAt, status }` (`storedTokens.mjs:49-68`). The id is usable **in place of the JWT** (`storedTokens.mjs:9-16`): - header `Authorization: Bearer {tokenId}`, or cookie `auth_token={tokenId}`, or query `?token={tokenId}`. The server resolves the id to its JWT/chain before authenticating (`server.mjs:183`, `getJwtByTokenId`). This is the "API token" surface: mint a stable, named, scoped, revocable handle that clients carry instead of a long JWT. Routes (`server.mjs:554-587`): `POST /auth/stored-tokens` (create), `GET /auth/stored-tokens[/:id]` (list/get), `GET /auth/stored-tokens/:id/jwt` (resolve), `DELETE /auth/stored-tokens/:id[/purge]` (revoke/purge).

## 8. Chains and minting (`src/chains.mjs`) Content-addressable store `auth/{domain}/chains/{sha256}.jwt`; each child's `parent` = hash of its parent JWT. Any holder mints an **attenuated** sub-token (scope ⊆ parent, expiry ≤ parent, depth ≤ `DEFAULT_MAX_DEPTH`) via `POST /auth/create-token` (`server.mjs:654`); verification walks leaf→root checking signature, `isScopeSubset`, expiry, depth, and the per-domain `revocations.json`. Routes under `/auth/chains` (`server.mjs:590-629`). This is the model in [`reference_token-minting-pattern`] and the auth bead [`9b2fcc1c`](https://redfish.acequia.io/guerin/.agents/9b2fcc1c-8960-49fb-ab6d-28b331a4e179/about.md).

## 9. CORS The WebDAV adapter sets `Access-Control-Allow-Origin: *`, `-Allow-Methods: *`, `-Allow-Headers: *`, `-Expose-Headers: *`, and `Access-Control-Allow-Private-Network: true` on every response (`serverFactories.mjs:79-85`), so browser acequia clients are first-class.

## 10. Putting it together — provisioning a multi-domain node 1. `config.local.mjs`: set `domainAlias` (host → domain), `mountPoints` (path → fs/mem + auth), SSL. 2. Point DNS / tunnel for each hostname at the server; the Host header selects the domain at runtime. 3. First registrant on a domain claims ownership (`subdomain.json`); set `authMode: user`. 4. Users register (`/auth/users/register`), get role + `paths`/`writePaths`; endorse device keys. 5. For programmatic clients, mint a **chain token** (`/auth/create-token`) and optionally wrap it as a **stored/API token** (`/auth/stored-tokens`) for a short bearer id; revoke by non-renewal or `revocations.json`. 6. For public reads, drop `.acequia-access.json` (`read: anonymous`) in the directory.

## Git / versions The clone is a git working tree on branch `alpha` tracking `origin/alpha` (sparse: `localWebDAV/`). As of 2026-06-18 it is current with the remote (no local edits). To update: `git -C <repo> fetch origin alpha && git -C <repo> merge --ff-only origin/alpha`. To fork an experiment, branch from `alpha` in this same clone and push to a fork remote; the sparse scope keeps it to `localWebDAV/`.

## References (bead cross-links) - Bead: Acequia Nephele · [canonical](https://redfish.acequia.io/guerin/.agents/1bd0d6c3-4483-4364-8a57-780c0e32df7d/) - Bead: Acequia Authorization · [canonical](https://redfish.acequia.io/guerin/.agents/9b2fcc1c-8960-49fb-ab6d-28b331a4e179/)