Adds command-line flags to zddc-server alongside the existing env vars.
Each setting can be set via --<flag-name> or ZDDC_<NAME>; the flag wins
on conflict, the env var wins over the hard-coded default.
--root / ZDDC_ROOT (now defaults to CWD if both unset)
--addr / ZDDC_ADDR (:8443)
--tls-cert / ZDDC_TLS_CERT ("none" / empty / path)
--tls-key / ZDDC_TLS_KEY
--log-level / ZDDC_LOG_LEVEL (info)
--index-path / ZDDC_INDEX_PATH (.archive)
--email-header / ZDDC_EMAIL_HEADER (X-Auth-Request-Email)
--cors-origin / ZDDC_CORS_ORIGIN (https://zddc.varasys.io; "" disables)
--insecure-direct / ZDDC_INSECURE_DIRECT (false)
--help (prints flag list to stderr, exits 0)
--version (prints binary + embedded tool versions, exits 0)
So an operator can `cd /srv/zddc && zddc-server` with zero config — the
served root defaults to the current directory, and TLS defaults to a
self-signed cert. config.Load now takes []string (test-friendly: nil
skips flag parsing entirely; tests pass an empty slice for env-only
loads).
Adds a `version` package-level var in main.go injected at link time via
`-ldflags="-X main.version=..."`. The build.sh runs git describe against
zddc-server-v* tags; for in-flight commits between releases it produces
e.g. zddc-server-v0.0.7-19-gadb6904-dirty.
Adds an embedded versions manifest:
- Each tool's compute_build_label (in shared/build-lib.sh) writes a
sidecar <tool>.label to $BUILD_LABELS_DIR if that env var is set.
- Top-level build.sh sets BUILD_LABELS_DIR before running each tool's
build, then assembles zddc/internal/apps/embedded/versions.txt as
one `<app>=<build label>` line per app.
- apps.EmbeddedVersions() loads the manifest at runtime.
- main.go logs a compact summary on every startup; --version dumps
the full per-app label.
Removes the old cfg.BuildVersion field — the X-ZDDC-Source: embedded
header now uses the package-level main.version directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds internal/apps/ package serving the five tool HTMLs at virtual paths
based on the surrounding folder name convention:
archive every directory (multi-project, project, archive, vendor)
classifier any Incoming/Working/Staging directory and subtree
mdedit any Working directory and subtree
transmittal any Staging directory and subtree
landing only at deployment root
The current-stable build of every tool is //go:embed'd into the binary
at compile time — that's the default with zero config. Operators
override per-directory via .zddc apps: entries; closer-to-leaf wins.
Spec syntax (in any apps: value):
stable / beta / alpha / :stable channel
v0.0.4 / v0.0 / v0 / :v0.0.4 version
https://my-mirror/releases URL prefix only
https://my-mirror/releases:beta URL prefix + channel
https://my-fork/archive.html terminal full URL
./local.html / /abs/path.html terminal local path
The special apps.default key provides a baseline URL prefix and channel
inherited by any app not overridden per-name. Per-axis cascade: a deeper
.zddc can override the URL, the channel, or both.
Cascade walks root→leaf; default applies first at each level, then the
per-app entry. Terminal sources (paths and full .html URLs) short-circuit
composition; deeper non-terminal entries override parent terminals.
URL sources fetch once on first request and cache forever in
<ZDDC_ROOT>/_app/<host>/<path> — different upstreams with the same
filename stay distinct. No background refresh, no SHA-256 verification:
operators delete the cache file to force a refetch. Concurrent misses
for the same source dedupe via a 30-line hand-rolled singleflight.
Per-request override: any user can append ?v=<spec> to a tool URL
(e.g. ?v=beta, ?v=v0.0.4, ?v=:alpha, ?v=https://mirror/releases:beta)
to ask for a different build for one request. Security: ?v= serves
ONLY versions already in the cache (cache miss returns 404; path
sources are rejected outright with 400). Users cannot trigger
arbitrary upstream fetches via crafted URLs.
Failed URL fetches (network down, 5xx) fall back to embedded with a
one-time WARN log. The X-ZDDC-Source response header reports what
served: fetch:URL / cache:URL / path:/abs / embedded:<app>@<build>.
Wire-in (cmd/zddc-server/main.go): dispatch routes <dir>/<app>.html
through apps.MatchAppHTML + AppAvailableAt + apps.Server.Serve when
no real file exists. Direct URL access to /_app/... is blocked at
the dispatch layer — cached files must go through the apps resolver
so they get correct Content-Type and ACL gating.
Schema (internal/zddc/file.go): ZddcFile gains Apps map[string]string
for cascade overrides. Validator (internal/zddc/validate.go) accepts
the special "default" key alongside the five canonical app names and
all spec forms.
Removes ZDDC_APPS_* env vars (no admin UI, no refresh interval, no
upstream allow-list — the simpler model has fewer knobs).
40+ unit tests across the new package: parser shapes, cascade
resolution with default+per-app interactions, terminal short-circuit
semantics, ?v= cache-only enforcement, embedded fallback, atomic
cache writes, singleflight dedup. Plus end-to-end dispatch tests in
cmd/zddc-server/main_test.go.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The middleware chain in main.go was:
AccessLogMiddleware ( CORSMiddleware ( ACLMiddleware ( dispatch ) ) )
ACLMiddleware extracts the user email from the configured header and
stores it in the request context via r.WithContext. But Go's context
propagates DOWN the chain (to handlers further in) — not back UP. The
new context-bearing request only exists inside the call to
next.ServeHTTP; once that returns, the outer middleware still has the
ORIGINAL request without the email. So AccessLogMiddleware's
EmailFromContext(r) call (which runs after next.ServeHTTP returns to
log the request) read from the original context and got an empty
string, falling through to "anonymous".
The /.profile/ page worked correctly because it reads the email
directly inside the handler — at that depth the context-bearing
request is the one in scope.
Fix: invert the chain so ACL is OUTERMOST.
ACLMiddleware ( AccessLogMiddleware ( CORSMiddleware ( dispatch ) ) )
Now ACL extracts the email and the new request flows down through
AccessLog (which sees the email-bearing context), CORS, and dispatch.
Add three regression tests in middleware_test.go:
TestAccessLogReadsEmailFromACLContext
The fix: with ACL outer, AccessLog logs email=alice@example.com
when X-Auth-Request-Email is set.
TestAccessLogAnonymousWhenNoEmail
The unchanged path: no header → email=anonymous (correct fallback).
TestAccessLogOuterDoesNotSeeInnerContext
Locks down Go's actual context-propagation behavior. Builds the
INVERTED (buggy) chain and asserts that AccessLog (outer) does NOT
see the email ACL (inner) set. If this ever fails, Go's context
propagation has changed in a way that lets inner-set context flow
upward — which would mean the reordering fix could be reverted.
All zddc-server tests pass via `go test ./...` (run in podman against
golang:1.24-alpine since this dev host doesn't have Go installed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the super-admin-only /.admin/ surface with a public-by-default
/.profile/ page that layers admin tools server-side based on the
caller's effective access:
- Universal (everyone, anonymous included): identity card, effective
access summary, theme picker, localStorage utilities (export / import
/ clear, landing-presets viewer).
- Subtree admins additionally see: editable .zddc files list (linking
to the existing form-based editor) and a "Create new project folder"
form.
- Super-admins additionally see: server config, log viewer, whoami
headers (the old /.admin/ JSON endpoints, repointed under /.profile/).
Project creation is gated on CanEditZddc(newDir) — the same strict-
ancestor rule that already governs .zddc writes — so no new authority
concept is introduced. ValidateProjectName mirrors the existing
reserved-prefix policy (no leading '.' or '_', no path separators).
/.admin/* is hard-cut: no redirect shim. Old URLs fall through to the
existing dot-prefix guard and 404. Custom CSS file rename: prefer
<root>/.profile.css, fall back to legacy <root>/.admin.css.
Per-resource 404 leakage gates preserved on whoami / config / logs /
zddc / projects so non-admin callers cannot detect the existence of
admin-only sub-resources.
Tree-wide gofmt -w applied as a side-effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three improvements bundled because they all ship as zddc-server v0.0.2:
* /.admin/ debug dashboard with /whoami, /config, /logs sub-routes.
Authorization via a top-level `admins:` glob list in <ZDDC_ROOT>/.zddc
(root-only — subdir entries deliberately ignored to prevent privilege
escalation via subtree write access). Non-admin requests get 404 so the
page is invisible. Recent logs surface via a 500-entry slog ring buffer
teed off the existing TextHandler. Lets operators debug without
kubectl exec.
* Default ZDDC_EMAIL_HEADER changes from `X-Email` to
`X-Auth-Request-Email` — the oauth2-proxy / nginx auth-request
convention that the TND helm chart already sets explicitly.
Operators who set the env var explicitly are unaffected; deployments
relying on the previous default need to set ZDDC_EMAIL_HEADER=X-Email
or update their proxy.
* dispatch() rejects any URL whose segments contain a dot prefix other
than the recognized virtual prefixes (.admin, cfg.IndexPath /
.archive). Matches the existing listing-pipeline filter so hidden
subtrees on the served PVC (e.g. /srv/.devshell — used by the
in-cluster dev-shell for persistent home-dir state) become
unreachable via direct HTTP fetch, not just hidden in listings.
Refreshes the X-Email reference in website/index.html accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.
See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.