# WebDAV Server (localWebDAV)

> **Platform Reference** — describes implemented, production behavior.

Node.js WebDAV server built on [Nephele](https://github.com/sciactive/nephele), providing authenticated file access per domain with JWT-based authorization, capability delegation chains, and sidecar-based public access control.

---

## Server Architecture

### Express + Nephele

The server is an Express app (`server.mjs`) with auth and API routes mounted before the Nephele WebDAV handler. Nephele handles all standard WebDAV methods (GET, PUT, DELETE, MKCOL, PROPFIND, COPY, MOVE, LOCK, etc.) against the filesystem.

```
Request flow:
  → PNA preflight handler
  → CORS
  → compression + cookie parsing
  → domain extraction + token extraction
  → token ID resolution (CUID2 → JWT)
  → site setup check
  → /auth/* API routes (Express JSON handlers)
  → static file serving (/public)
  → session timeout redirect
  → cross-server COPY/MOVE middleware
  → domain directory check
  → Nephele WebDAV server
```

### Domain Isolation

Each domain gets its own filesystem root under `acequia/{domain}/`. The domain is extracted from the `Host` header as the full hostname (e.g., `stigmergic.acequia.live`). All WebDAV operations are scoped to that directory. Auth data lives in `auth/{domain}/`.

For local development, `config.domainAlias` maps local hostnames (e.g., `stigmergic.localhost`) to canonical domains (e.g., `stigmergic.acequia.live`).

The server supports **multi-domain hosting** — multiple parent domains (e.g., `*.acequia.io` and `*.santafe.live`) on the same instance, with SNI-based SSL certificate selection when `config.ssl` is an array of `{ domains, key, cert }` entries.

### Nephele Configuration (`src/serverFactories.mjs`)

`makeServer()` creates a Nephele instance with:

- **Adapter:** `ScopedFileSystemAdapter` — extends Nephele's `FileSystemAdapter` with per-resource JWT scope enforcement and dotfile filtering
- **Authenticator:** `JWTAuthenticator` — three-mode JWT verification (chain, user, legacy)
- **Plugins:**
  - `DirectoryJsonPlugin` — JSON directory listings via `X-Directory-As-JSON` header, dotfile/scope filtering, trailing-slash redirect
  - `IndexPlugin` — HTML directory listings with upload/mkdir UI (Nephele built-in)

### ScopedFileSystemAdapter

Extends Nephele's `FileSystemAdapter.isAuthorized()` with two additional checks before falling through to Unix permission checks:

1. **Dotfile filtering** — Files starting with `.` are hidden from users without write access to that path. Exception: `.well-known`, `.ai`, `.acequia-access.json` are always visible.

2. **Scope filtering** — Users with explicit path restrictions (e.g., `paths: ['/ants/*']`) can only see resources within their scope. COPY and PROPFIND are classified as read methods; PUT, DELETE, MOVE as write methods. Ancestor directories are visible (for navigation) but only show in-scope children.

### DirectoryJsonPlugin (`src/plugins/directoryJson.mjs`)

Wraps `resource.getInternalMembers()` to filter both JSON and HTML directory listings. Runs before IndexPlugin in the plugin chain. Three behaviors:

1. **Dotfile filtering** — Same allowlist as the adapter, applied to listing entries
2. **Scope filtering** — Only show entries matching the user's allowed paths
3. **JSON mode** — When `X-Directory-As-JSON` header is present, returns `[{ filename, basename, type, size, lastmod, mime }]` instead of HTML

Also handles trailing-slash redirects for directory URLs so that relative upload URLs resolve correctly.

---

## Authentication (`src/auth/jwtAuth.mjs`)

Three-mode JWT routing based on claim inspection:

| Mode | Identifying Claim | Verification | Access Control |
|------|-------------------|-------------|----------------|
| **Chain** | `parent` present | Walk chain to root, verify each signature | Scope from chain's `paths`/`writePaths` |
| **User** | `kid` present, no `parent` | Verify against user's registered public key | Role-based (owner/editor/viewer) |
| **Legacy** | Neither `kid` nor `parent` | Verify against device public key file | Full access |

### Sidecar Public Access

`.acequia-access.json` files control anonymous read access per directory:

```json
{
  "read": "anonymous",
  "recursive": true,
  "denyPatterns": ["*.secret", "internal/**"]
}
```

The authenticator walks up ancestor directories looking for sidecars (nearest wins). Results are cached with 60s TTL. Anonymous users get `writePaths: []`, which feeds into dotfile filtering.

---

## Cross-Server COPY (`src/crossServerCopy.mjs`)

Express middleware mounted before Nephele that intercepts COPY/MOVE requests with foreign `Destination` headers.

### Protocol: TPC TransferHeader Convention

From CERN's grid storage community. The client sends two tokens:

```
Authorization: Bearer <source-token>
TransferHeaderAuthorization: Bearer <destination-token>
Destination: https://dest-server.com/path/file.txt
```

The middleware strips the `TransferHeader` prefix and forwards to the destination:

```
PUT /path/file.txt
Authorization: Bearer <destination-token>
```

### Same-Host vs Cross-Server

- **Same host** (Destination matches `req.headers.host`) → passes through to Nephele, which uses kernel-level `fsp.copyFile()` (zero-copy)
- **Different host** → middleware handles: reads source from local filesystem, streams to destination via `fetch` PUT

### Implementation Details

- **Streaming:** `createReadStream()` → `fetch` with `duplex: 'half'`. `Content-Length` set from `stat.size`.
- **Directories:** Recursive MKCOL for directories, bounded concurrent PUT for files (default: 4 parallel)
- **Concurrency:** Global semaphore limits concurrent outbound transfers to 4 per server instance
- **Timeouts:** Per-file: 30s + 1s per MB. Client disconnect detected via `req.on('close')` → `AbortController`
- **Loop detection:** `fsp.realpath()` tracking + max depth of 50

### Error Responses

| Scenario | Response |
|----------|----------|
| Destination unreachable | 502 Bad Gateway |
| Destination auth rejected | 207 with per-resource 401/403 |
| Partial recursive failure | 207 Multi-Status |
| Destination exists + `Overwrite: F` | 412 Precondition Failed |

See [webdav-cross-server-copy.md](projects/webdav-cross-server-copy.md) for the full design document.

---

## Auth API Routes

All mounted under `/auth/` with Express JSON body parsing.

### Users (`src/users.mjs`)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/auth/users/register` | public | Register new user with first device key |
| GET | `/auth/users` | owner | List all users |
| GET | `/auth/users/:userId` | user | Get user info |
| PATCH | `/auth/users/:userId` | owner | Update role/permissions |
| DELETE | `/auth/users/:userId` | owner | Remove user |
| GET | `/auth/users/:userId/keys` | user | List device keys |
| GET | `/auth/users/:userId/keys/:kid/public` | public | Get public key for verification |
| POST | `/auth/users/:userId/keys` | user | Add device key (endorsement) |
| PATCH | `/auth/users/:userId/keys/:kid` | user | Update key metadata (deviceName) |
| DELETE | `/auth/users/:userId/keys/:kid` | user | Revoke key |
| DELETE | `/auth/users/:userId/keys/:kid/purge` | user | Permanently delete revoked key |
| GET | `/auth/handles/:handle/available` | public | Check handle availability |

### Invites (`src/invites.mjs`)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/auth/invites` | owner | Create invite |
| POST | `/auth/invites/accept` | public | Accept invite and register |
| GET | `/auth/invites/:inviteId` | public | Get invite info |
| GET | `/auth/invites` | owner | List invites |
| DELETE | `/auth/invites/:inviteId` | owner | Revoke invite |
| DELETE | `/auth/invites/:inviteId/purge` | owner | Delete non-active invite |

### Device Links (`src/deviceLinks.mjs`)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/auth/device-link/accept` | token | Accept device link |
| POST | `/auth/device-link/info` | token | Get link info for validation |
| POST | `/auth/device-link/short-code` | token | Create short code for linking |
| GET | `/auth/device-link/short-code/:code` | public | Resolve short code |
| POST | `/auth/device-link/request` | public | Create reverse link request (QR) |
| GET | `/auth/device-link/request/:code` | public | Get pending request |
| POST | `/auth/device-link/request/:code/approve` | token | Approve reverse link |

### Delegation Chains (`src/chains.mjs`)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/auth/chains/id/:tokenId` | public | Fetch chain link by CUID2 ID |
| GET | `/auth/chains/:tokenHash` | public | Fetch chain link by SHA-256 hash |
| POST | `/auth/delegate` | user | Server-assisted delegation |
| PUT | `/auth/chains/:tokenHash` | user | Store chain link |
| GET | `/auth/chains` | user | List stored chain tokens |
| DELETE | `/auth/chains/:tokenHash` | user | Revoke chain |

### Revocations (`src/revocations.mjs`)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/auth/revocations` | owner | List revocations |
| POST | `/auth/revocations` | owner | Add revocation |
| DELETE | `/auth/revocations/:tokenHash` | owner | Remove revocation |

### Stored Tokens (`src/storedTokens.mjs`)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/auth/stored-tokens` | user | Create stored token |
| GET | `/auth/stored-tokens` | user | List tokens |
| GET | `/auth/stored-tokens/:tokenId` | user | Get token info |
| PATCH | `/auth/stored-tokens/:tokenId` | user | Update metadata |
| PUT | `/auth/stored-tokens/:tokenId/jwt` | user | Regenerate JWT |
| DELETE | `/auth/stored-tokens/:tokenId` | user | Revoke token |
| DELETE | `/auth/stored-tokens/:tokenId/purge` | user | Delete revoked token |

---

## Token ID Resolution

Short CUID2 IDs can be used in place of full JWTs for convenience (URLs, QR codes). Middleware at startup resolves them before authentication:

1. Check stored tokens (`auth/{domain}/stored-tokens/{id}.json`)
2. Check chain short IDs (`chainStore.loadTokenById`)
3. If neither matches, pass through as-is (may be a full JWT)

---

## Periodic Cleanup

Every 60 seconds, the server runs cleanup for:
- Expired short codes (device linking)
- Expired stored tokens
- Expired revocation entries
- Expired chain tokens
- Expired link requests

---

## Server Configuration

| Setting | Default | Description |
|---------|---------|-------------|
| `PORT` | 3333 | HTTP listen port |
| `WEBDAV_ROOT` | `./acequia` | Filesystem root for WebDAV content |
| `config.useDomain` | true | Extract full domain from Host header |
| `config.ssl` | null | HTTPS cert/key or array of `{ domains, key, cert }` for SNI (listens on 3334) |
| Socket timeout | 10 min | Idle socket timeout |
| Request timeout | 30 min | Total request timeout (large uploads) |
| Headers timeout | 5 min | Time to receive headers |
| `trust proxy` | true | Trust X-Forwarded-* headers (for HTTPS behind reverse proxy) |

Configuration is split between `config.mjs` (base, checked in) and `config.local.mjs` (local overrides, gitignored).

---

## Client Library Build Pipeline

The browser-side Acequia library lives in sibling repo `acequia2/`. After any change:

```bash
cd localWebDAV && yarn copyAcequiaBuild
```

This runs `cd ../acequia2/ && yarn install && yarn build`, then copies:

| Source (acequia2/) | Destination (public/) | Purpose |
|---|---|---|
| `sw.js` | `sw.js` | Service worker — VFS mounts, route matching, WebRTC proxy |
| `webdav.js` | `webdav.js` | Client-side WebDAV helpers |
| `build/acequia*` | `acequia.js` + sourcemaps | Rollup bundle — the `window.acequia` namespace |

The files in `public/` are git-tracked build artifacts. Never edit them directly.

---

## Static Files & Special Routes

| Route | Behavior |
|-------|----------|
| `/acequia.js`, `/webdav.js`, `/SharedWebRTCWorker.js` | Served directly (no auth redirect) |
| `/sw.js` | Served with `Cache-Control: no-cache, no-store` |
| `/config.local.js` | Served from WebDAV if exists, else empty `export const config = {}` |
| `?acequia-editor` | Redirects to `/editor.html#!/path` |
| `/status` | Health check JSON (name, version, uptime) |

---

## File Layout

```
server.mjs                      # Express app — middleware stack, route mounting
config.mjs                      # Base configuration (checked in)
config.local.mjs                # Local overrides (gitignored)
src/
  serverFactories.mjs           # Nephele adapter factory, ScopedFileSystemAdapter
  auth/jwtAuth.mjs              # JWTAuthenticator — 3-mode routing + sidecar access
  auth/utils.mjs                # hashToken, findTokensInRequest, matchesPath, isAncestorOfScope
  chains.mjs                    # Content-addressable chain store, verification, delegation
  revocations.mjs               # Per-domain revocation lists
  invites.mjs                   # Invite system — time-bound, scoped user provisioning
  users.mjs                     # User management — registration, keys, roles, handles
  deviceLinks.mjs               # Cross-device key linking via short codes + QR
  shortCodes.mjs                # 6-char short code generation
  storedTokens.mjs              # CUID2 → JWT resolution, chain hash linking
  crossServerCopy.mjs           # TPC TransferHeader cross-server COPY/MOVE
  siteAdmin.mjs                 # Site-level admin, setup, domain creation
  plugins/directoryJson.mjs     # JSON listings, dotfile/scope filtering
public/
  dashboard.html + dashboard.js # Owner dashboard
  editor.html + editor.js       # Standalone file editor
  logout.html                   # Identity reset with confirmation
  register-user.html            # Profile registration
  accept-invite.html            # Invite acceptance flow
  approve-link.html             # Device link approval
  components/                   # Web components (acequia-file-browser, acequia-editor)
  styles/                       # Design tokens (acequia-tokens.css)
  acequia.js                    # Built acequia library (from acequia2 — do not edit)
  sw.js                         # Built service worker (from acequia2 — do not edit)
  webdav.js                     # Built WebDAV helpers (from acequia2 — do not edit)
acequia/{domain}/                # WebDAV content root per domain
auth/{domain}/                   # Auth data per domain
  subdomain.json                # Owner, authMode, timestamps (filename kept for compat)
  users/{userId}.json           # User records
  handles/{handle}.json         # Handle → userId index
  invites/{inviteId}.json       # Invite metadata
  chains/{sha256}.jwt           # Content-addressable chain tokens
  revocations.json              # Revoked token hashes
  stored-tokens/{id}.json       # Stored token metadata
```
