Simplify docs and define configuration-first setup #51
No files matched your search
+2
-10
@@ -1,13 +1,5 @@
|
||||
# Changesets
|
||||
|
||||
- Run `pnpm changeset` with a feature or fix to record its app impact for
|
||||
`@directory/web` or `@directory/cli` (major, minor or patch).
|
||||
- `@directory/config` is internal and ignored for independent releases. Record
|
||||
behavior/schema changes in an explicit changeset for the consuming app(s).
|
||||
- When ready, manually run `pnpm version:apps` to consume pending changesets,
|
||||
synchronize the two private apps in lockstep and generate changelogs. Review
|
||||
and commit the release changes only with owner authorization.
|
||||
- Only after review and authorization, run `pnpm tag:apps` to create local Git
|
||||
tags; pushing them is a separate, explicitly authorized step.
|
||||
Run `pnpm changeset` for each change.
|
||||
|
||||
See the [manual release workflow](../docs/development.md#manual-app-releases).
|
||||
Follow the bunny.
|
||||
@@ -17,7 +17,6 @@
|
||||
**/e2e
|
||||
**/.env
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
**/.npmrc
|
||||
**/*.local.toml
|
||||
**/docker-compose*.local.yml
|
||||
|
||||
@@ -12,7 +12,6 @@ on:
|
||||
- .woodpecker/**
|
||||
- apps/cli/**
|
||||
- packages/**
|
||||
- apps/web/config/directory.example.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- '*'
|
||||
@@ -22,7 +21,6 @@ on:
|
||||
- .woodpecker/**
|
||||
- apps/cli/**
|
||||
- packages/**
|
||||
- apps/web/config/directory.example.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -10,7 +10,6 @@ on:
|
||||
- .github/workflows/**
|
||||
- .woodpecker/**
|
||||
- packages/config/**
|
||||
- apps/web/config/directory.example.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- '*'
|
||||
@@ -19,7 +18,6 @@ on:
|
||||
- .github/workflows/**
|
||||
- .woodpecker/**
|
||||
- packages/config/**
|
||||
- apps/web/config/directory.example.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
+1
-3
@@ -11,13 +11,11 @@ test-results/
|
||||
/.nono.json
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.local.toml
|
||||
docker-compose*.local.yml
|
||||
docker-compose*.local.yaml
|
||||
/config/where.yaml
|
||||
/apps/web/config/where.yaml
|
||||
/apps/web/config
|
||||
/config/directory.yaml
|
||||
/apps/web/config/directory.yaml
|
||||
/public/
|
||||
/apps/web/public/
|
||||
@@ -14,7 +14,6 @@ when:
|
||||
- .woodpecker/**
|
||||
- apps/cli/**
|
||||
- packages/**
|
||||
- apps/web/config/directory.example.yml
|
||||
- event: pull_request
|
||||
path:
|
||||
include:
|
||||
@@ -25,7 +24,6 @@ when:
|
||||
- .woodpecker/**
|
||||
- apps/cli/**
|
||||
- packages/**
|
||||
- apps/web/config/directory.example.yml
|
||||
|
||||
- event: tag
|
||||
ref: 'refs/tags/@directory/cli@*'
|
||||
|
||||
@@ -13,7 +13,6 @@ when:
|
||||
- .github/workflows/**
|
||||
- .woodpecker/**
|
||||
- packages/config/**
|
||||
- apps/web/config/directory.example.yml
|
||||
- event: pull_request
|
||||
path:
|
||||
include:
|
||||
@@ -23,7 +22,6 @@ when:
|
||||
- .github/workflows/**
|
||||
- .woodpecker/**
|
||||
- packages/config/**
|
||||
- apps/web/config/directory.example.yml
|
||||
|
||||
steps:
|
||||
prepare:
|
||||
|
||||
@@ -1,29 +1,3 @@
|
||||
# Working on Directory
|
||||
|
||||
Start with [README.md](README.md) and [docs/development.md](docs/development.md).
|
||||
Local planning notes, when present, live in `.plans/foundation/architecture.md`
|
||||
and `.plans/foundation/implementation.md`. They are not build inputs.
|
||||
|
||||
## Review and delivery
|
||||
|
||||
- Work in small, meaningful commit-sized changes and reasonably small PRs.
|
||||
- Complete agent code review before handing changes to the owner.
|
||||
- When the owner requests staged review, stage only one coherent commit at a
|
||||
time and wait for their review before preparing the next staged commit.
|
||||
- Do not commit or push without the owner's manual review and explicit
|
||||
authorization. Permission to stage is not permission to commit or push.
|
||||
- At each handoff, report checks actually run, outstanding limitations, and useful
|
||||
manual tests beyond code review. Never describe an unrun check as passing.
|
||||
- Preserve operator configuration, prototype files, and unrelated changes.
|
||||
|
||||
## Engineering
|
||||
|
||||
- Use the pinned Node and pnpm versions. Keep dependency versions exact; shared
|
||||
tools use the workspace catalog.
|
||||
- Use strict TypeScript, ESLint with Svelte rules, and Prettier.
|
||||
- Keep tests beside the code as `*.test.ts`; browser tests live in `apps/web/e2e`.
|
||||
- Keep server code out of browser imports. Settings are configuration, not a
|
||||
database or an admin editor.
|
||||
- Keep comments for necessary context; do not narrate straightforward code.
|
||||
- Do not add speculative abstractions, a worker, live service integrations, or
|
||||
a second lint/format toolchain.
|
||||
Read [README.md](README.md) and all the documentation in the docs folder.
|
||||
@@ -1,134 +1,40 @@
|
||||
# 🗂️ Directory
|
||||
|
||||
A minimal homelab directory for the family: learn what is available and open the
|
||||
right link. The dashboard will be driven by one mounted YAML file.
|
||||
A bare bones directory page for your homelab.
|
||||
|
||||
## Current state
|
||||

|
||||
|
||||
The app includes a server-rendered directory, a compiled shared config
|
||||
package, a standalone validator, and per-package CI checks. The shared loader reads YAML, applies
|
||||
defaults, expands environment values, and validates the TypeBox contract. The
|
||||
standalone CLI uses that same loader. It normalizes omitted site logo and favicon
|
||||
paths to bundled `/favicon.svg`; explicit bundled selections are also valid.
|
||||
The web server routes mounted site selections through public branding aliases.
|
||||
The web server loads an immutable directory
|
||||
before serving, polls for file changes, and keeps the last good snapshot after
|
||||
invalid edits. Open pages with JavaScript check for accepted updates and offer
|
||||
Refresh without automatically replacing what someone is reading. Compact icon-and-title tiles flow into a responsive grid with muted
|
||||
group headings, plain shareable details and real external service links.
|
||||
Header search loads on first focus/input and finds bookmarks with typo-tolerant,
|
||||
word-order-independent matching across public bookmark metadata. Results rank by
|
||||
relevance; clearing restores the configured groups and order. Enter opens details
|
||||
for one ready result, or focuses the first when several match. Down enters results;
|
||||
Tab and Shift+Tab visit every result, with arrow keys as an optional shortcut.
|
||||
Escape returns to Search without losing the query. Escape in Search clears it. Search stays local
|
||||
to the loaded directory and is disabled without JavaScript; the full directory
|
||||
and real detail URLs still work.
|
||||
Directory does one thing and nothing more. That's the point. Sort of.
|
||||
|
||||
Use `tag:family photos` for an exact, case-insensitive whole tag, or `access:login`,
|
||||
`access:internal` and `access:admin` for enabled advisory flags, not access rules.
|
||||
Selecting a tag or access chip in details closes details and fills Search with that
|
||||
single query. Search is preserved in the URL's `q` parameter for sharing and reloads;
|
||||
Clear removes it and restores grouped browsing. Without JavaScript, chips remain
|
||||
informational and the full directory and plain details stay usable.
|
||||
No plugins. No database. No baloney.
|
||||
|
||||
The header theme button cycles System → Dark → Light → System and saves the
|
||||
choice in this browser. System follows OS changes; without JavaScript the OS
|
||||
scheme applies and the button stays disabled. Existing
|
||||
`appearance.primary_light_color` and `primary_dark_color` configure keyboard-focus
|
||||
and Back-link underline accents on the directory, plus the filled sign-in action.
|
||||
These two colors are public branding inputs. Sign-in automatically chooses a
|
||||
readable foreground; choose colors with sufficient contrast for focus accents too.
|
||||
Authentication is optional: shared credentials or OIDC protect the whole directory,
|
||||
including data responses. Bookmark cards and details use server-prepared bookmark
|
||||
logos when no logo path is configured. With JavaScript, a failed configured bookmark
|
||||
logo switches once to its prepared logo; a failed fallback stops without retrying.
|
||||
Without JavaScript, broken configured images stay broken, but text and links remain
|
||||
usable. After repairing an image at the same URL, refresh to try it again.
|
||||
The directory header shows `site.logo_path` at 48px beside its title and description,
|
||||
using bundled `/favicon.svg` when omitted or once after a JavaScript image error.
|
||||
Without JavaScript, a broken configured image stays broken. Sign-in shows the same
|
||||
selected logo through public `/logo`, with the bundled 🗂️ mark as its default and
|
||||
one-time JavaScript fallback. Selecting `site.logo_path` deliberately publishes
|
||||
those bytes before login; the configured source URL stays private.
|
||||
Configured images are now served only when
|
||||
referenced by the captured configuration, with bounded 5 MiB reads and the same
|
||||
authentication policy as the directory for mounted source URLs. Selecting
|
||||
`site.favicon_path` publishes that image's bytes at the public `/favicon` alias,
|
||||
including before sign-in; its source URI stays out of the shared layout. Omission
|
||||
uses the bundled `/favicon.svg`; unavailable configured images return 404, not a
|
||||
bundled fallback. Mounted files require Linux procfs; SVGs are trusted operator
|
||||
content, not sanitized uploads. See [authentication setup](docs/config.md#authentication-shapes).
|
||||
See the [configuration contract](docs/config.md) and
|
||||
[production starter](apps/web/config/directory.example.yml). For the isolated,
|
||||
local-only 32-bookmark review, see [the demo](demo/README.md).
|
||||
## Get started
|
||||
|
||||
## Run with Docker
|
||||
Let's assume you have docker compose (or you are getting it RIGHT NOW or you know
|
||||
your way around the docker cli or you have your own tricks up your sleeve...)
|
||||
|
||||
Follow [local production review](docs/deployment.md#local-production-review) to
|
||||
bootstrap the minimal, empty production starter without overwriting operator files.
|
||||
It has open access and needs no session secret; enable authentication before
|
||||
publishing. No host Node/pnpm is needed. For a populated local review instead,
|
||||
follow [the isolated demo quickstart](demo/README.md).
|
||||
|
||||
## Development with Node
|
||||
|
||||
Use Node **26.5.0** and pnpm **11.9.0**, pinned in `mise.toml`:
|
||||
Run the following commands in your favorite terminal:
|
||||
|
||||
```sh
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm --filter @directory/web exec playwright install chromium
|
||||
pnpm verify
|
||||
CONFIG_PATH="$PWD/apps/web/config/directory.example.yml" pnpm dev
|
||||
mkdir directory
|
||||
cd directory
|
||||
mkdir config public
|
||||
curl -fsSLo compose.yaml https://git.eleith.com/eleith/directory/raw/branch/main/docs/compose.yaml
|
||||
curl -fsSLo config/directory.yaml https://git.eleith.com/eleith/directory/raw/branch/main/docs/directory.example.yaml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Run from the repository root and open `http://localhost:5173`. This starter is
|
||||
empty and has no authentication. For the populated, authenticated preview that
|
||||
leaves operator configuration and media untouched, use [demo/README.md](demo/README.md).
|
||||
Groups, bookmark titles and `?link=<id>` details also render without JavaScript.
|
||||
The desktop dialog and phone sheet code loads with the page, so opening details
|
||||
has no additional code-loading wait. Direct links animate in without a stretched
|
||||
preview, and closing plays the matching exit. Plain details remain available without
|
||||
JavaScript, with browser history and keyboard focus preserved after enhancement.
|
||||
`/healthz` returns generic readiness. Invalid auth environment settings stop startup
|
||||
with `auth_failed` and a fixed, value-free setting hint. Linux prerequisites,
|
||||
reload checks and shutdown are documented in [development](docs/development.md).
|
||||
Meditate for 5s or 1m depending on your internet connection, cpu speed or room
|
||||
temperature.
|
||||
|
||||
Validate the production starter, isolated demo or your own configuration:
|
||||
Open http://localhost:3000.
|
||||
|
||||
```sh
|
||||
pnpm cli -- --help
|
||||
pnpm cli -- validate apps/web/config/directory.example.yml
|
||||
pnpm cli -- validate demo/config/directory.yaml
|
||||
```
|
||||
Rejoice.
|
||||
|
||||
Validation returns 0 for a valid file and nonzero for unreadable or invalid input.
|
||||
It checks environment references without printing configuration values. See
|
||||
[configuration](docs/config.md) for paths, interpolation and safe errors, and
|
||||
[agent instructions](AGENTS.md) for the review workflow.
|
||||
Edit `config/directory.yaml` to start adding your links.
|
||||
|
||||
## Continuous integration
|
||||
## Documentation
|
||||
|
||||
[Woodpecker](.woodpecker/) and [GitHub Actions](.github/workflows/) each have
|
||||
`config.yml`, `cli.yml`, and `web.yml`, following `when`'s per-package pattern.
|
||||
Package-scoped checks use frozen installs; shared-package and root-tooling
|
||||
changes trigger dependent pipelines. Web E2E reuses the matching Playwright
|
||||
images. `pnpm verify` remains a local all-workspace convenience, not a pipeline.
|
||||
Main builds publish Linux amd64 `latest` images to the confirmed Gitea and GHCR
|
||||
registries after web/E2E and schema/config/CLI gates. Qualified stable web tags
|
||||
publish versioned images after the same gates; PRs do not publish. Automatic
|
||||
deployment is not included.
|
||||
|
||||
Each Woodpecker pipeline has one status-aware email step for main pushes, using `when`'s
|
||||
notification secret names. Confirm those bindings, publishing authorization and Gitea CI-image pull access
|
||||
on the actual provider; a Gitea PR does not itself trigger GitHub Actions. See
|
||||
[development](docs/development.md) for routing, safe secret setup, first-run
|
||||
checks, and scope limits.
|
||||
|
||||
## Releases
|
||||
|
||||
Changesets records app changes; the private web and CLI apps version together.
|
||||
Versioning, release commits and tag pushes are manual and require owner review
|
||||
and authorization. Only stable `@directory/web@<version>` tags publish versioned
|
||||
images; `latest` remains main-only. See the
|
||||
[release workflow](docs/development.md#manual-app-releases).
|
||||
- [Configuration](docs/config.md)
|
||||
- [Deployment](docs/deployment.md)
|
||||
- [Development](docs/development.md)
|
||||
@@ -1,25 +0,0 @@
|
||||
# Copy to .env beside the Compose files. Never commit real secrets.
|
||||
# No-auth configurations require no session secret.
|
||||
# If auth is enabled, set your own random AUTH_SECRET of at least 32 characters;
|
||||
# there is no shared fallback. Generate once with: openssl rand -base64 32
|
||||
# Save the result below in your ignored .env before starting Compose.
|
||||
# AUTH_SECRET=
|
||||
# For the isolated local review demo, see demo/README.md at the repository root.
|
||||
|
||||
# For production behind your HTTPS proxy, set the public root origin.
|
||||
# Leave unset for local review: Compose selects localhost:3000 or localhost:5173.
|
||||
# Compose sets AUTH_URL to this same origin.
|
||||
# ORIGIN=https://directory.example.com
|
||||
|
||||
# Optional values for custom YAML that references them.
|
||||
# DIRECTORY_PASSWORD=
|
||||
# DIRECTORY_OIDC_CLIENT_SECRET=
|
||||
|
||||
# Optional image source and host-port overrides.
|
||||
# DIRECTORY_IMAGE=ghcr.io/eleith/directory:latest
|
||||
# DIRECTORY_PORT=3000
|
||||
# DIRECTORY_BIND=127.0.0.1
|
||||
|
||||
# Development containers use your host identity; override if it is not 1000:1000.
|
||||
# LOCAL_UID=1000
|
||||
# LOCAL_GID=1000
|
||||
@@ -1,6 +0,0 @@
|
||||
# yaml-language-server: $schema=../../../packages/config/src/config.schema.json
|
||||
# Minimal production starter: open access, no credentials or session secret required.
|
||||
# Configure authentication before publishing. See docs/config.md for the contract.
|
||||
# For the isolated local review demo, see demo/README.md at the repository root.
|
||||
version: 1
|
||||
links: {}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
# Isolated local review demo
|
||||
|
||||
This is a **manual, local-only** preview, not production configuration. Sign in
|
||||
with **demo / directory-demo** using real Auth.js credentials authentication.
|
||||
Never publish these known credentials. A unique generated `AUTH_SECRET` is still
|
||||
required; there is no shared secret fallback.
|
||||
|
||||
The 32 bookmarks span five uneven groups (10, 7, 6, 5 and 4 links), with short,
|
||||
long and unbroken titles for layout review. All bookmarks use generated default
|
||||
logos; the header, sign-in mark and tab icon use bundled default branding. No
|
||||
image mounts or asset copies are needed. Existing SVGs in `public/demo` are
|
||||
optional, unreferenced media, not preview requirements. Destinations are reserved
|
||||
example.com placeholders, not live integrations.
|
||||
|
||||
Check the overall centered canvas at desktop widths: header, group headings,
|
||||
first grid column and footer share a left edge. Short groups and incomplete rows
|
||||
start at that same column rather than centering separately. Check phone widths,
|
||||
light/dark themes, long titles and search results too. All 32 cards and plain
|
||||
`?link=<id>` details should work with JavaScript on and off; generated bookmark
|
||||
images must stay private after sign-out. The header title should wrap without
|
||||
hiding Sign out. Dialog and form redesign are outside this page-layout review.
|
||||
|
||||
## Preferred native preview
|
||||
|
||||
With the [development prerequisites](../docs/development.md#setup-and-commands)
|
||||
installed, run from the repository root:
|
||||
|
||||
```sh
|
||||
CONFIG_PATH="$PWD/demo/config/directory.yaml" \
|
||||
AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
ORIGIN=http://localhost:5173 AUTH_URL=http://localhost:5173 pnpm dev
|
||||
```
|
||||
|
||||
Open **http://localhost:5173**, sign in, and stop with Ctrl-C when finished. This
|
||||
command writes no operator configuration or media. Each run generates an ephemeral
|
||||
secret and invalidates previous sessions; real deployments should keep their own
|
||||
generated secret stable. Use localhost, not an HTTP IP address. If changing the
|
||||
port, update both `ORIGIN` and `AUTH_URL` to match.
|
||||
|
||||
For a production-server preview after `pnpm build`, from the repository root:
|
||||
|
||||
```sh
|
||||
CONFIG_PATH="$PWD/demo/config/directory.yaml" \
|
||||
AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
ORIGIN=http://localhost:3000 AUTH_URL=http://localhost:3000 \
|
||||
HOST=127.0.0.1 PORT=3000 node apps/web/build/index.js
|
||||
```
|
||||
|
||||
Open **http://localhost:3000** with the same demo credentials. To test reloads,
|
||||
copy `demo/config/directory.yaml` to a disposable directory and point `CONFIG_PATH`
|
||||
to that absolute copy. Invalid edits keep the last good snapshot. Auth changes
|
||||
require a process restart.
|
||||
|
||||
Validate both the empty production starter and this demo manually from the repository root:
|
||||
|
||||
```sh
|
||||
pnpm cli -- validate apps/web/config/directory.example.yml
|
||||
pnpm cli -- validate demo/config/directory.yaml
|
||||
```
|
||||
|
||||
The CLI validates YAML/interpolation, not web auth prerequisites or image bytes.
|
||||
|
||||
## Optional Docker review
|
||||
|
||||
Use the existing Compose files; there is no demo-specific Compose configuration.
|
||||
From `apps/web`, create `.env` only if absent:
|
||||
|
||||
```sh
|
||||
test -e .env || cp -n .env.example .env
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Manually save that result as `AUTH_SECRET=<result>` in your ignored `.env`, or
|
||||
keep a valid unique secret already supplied by your environment/fnox. Do not
|
||||
replace an existing stable secret just for setup.
|
||||
|
||||
**Stop the server and back up any existing `config/directory.yaml` yourself
|
||||
before explicitly choosing the following replacement.** This is a manual demo
|
||||
opt-in, not an automatic bootstrap. The native preview above leaves active files
|
||||
alone. Still in `apps/web`, copy only the configuration; no images are required:
|
||||
|
||||
```sh
|
||||
cp ../../demo/config/directory.yaml config/directory.yaml
|
||||
ORIGIN=http://localhost:3000 docker compose up --build --pull never --force-recreate
|
||||
```
|
||||
|
||||
Open **http://localhost:3000** and sign in with **demo / directory-demo**. The
|
||||
explicit origin overrides any production HTTPS origin in `.env` for local review.
|
||||
For the existing dev-container workflow instead, use:
|
||||
|
||||
```sh
|
||||
ORIGIN=http://localhost:5173 docker compose -f docker-compose.dev.yml up --build --force-recreate
|
||||
```
|
||||
|
||||
With mise/fnox already configured, the equivalent dev shortcut from `apps/web` is
|
||||
`ORIGIN=http://localhost:5173 mise docker up --build --force-recreate`; keep the
|
||||
existing valid secret supply. Stop one Compose setup before starting the other.
|
||||
See [deployment](../docs/deployment.md) for shutdown, mounts and HTTPS setup.
|
||||
Leave existing operator images untouched.
|
||||
|
||||
## Scope and automated-check exclusion
|
||||
|
||||
`demo/` is tracked for manual local previews only. It has no package and is
|
||||
outside the `apps/*` and `packages/*` workspaces. Automated tests use their own
|
||||
fixtures, never this demo. It is not an automated test, build, lint, formatting or
|
||||
package input, does not trigger CI, and is excluded from the Docker build context.
|
||||
The minimal, credential-free production starter remains in `apps/web/config` for
|
||||
operator bootstrap; it is not this demo. Enable your own authentication before
|
||||
publishing.
|
||||
@@ -14,8 +14,8 @@ appearance:
|
||||
|
||||
auth:
|
||||
credentials:
|
||||
username: demo
|
||||
password: directory-demo
|
||||
username: admin
|
||||
password: directory
|
||||
|
||||
# Reserved example.com destinations are placeholders, not live integrations.
|
||||
# All bookmarks use generated logos; site branding uses the bundled default.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
directory-app:
|
||||
# Replace latest with a published version or digest to pin updates.
|
||||
image: ghcr.io/eleith/directory:latest
|
||||
platform: linux/amd64
|
||||
# Optional values for ${VAR} references in directory.yaml.
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
ports:
|
||||
# Localhost only. Use '3000:3000' for LAN access; restrict it with your firewall.
|
||||
# Change the left port to use a different host port, e.g. '127.0.0.1:8080:3000'.
|
||||
- '127.0.0.1:3000:3000'
|
||||
volumes:
|
||||
- ./config:/app/config:ro
|
||||
- ./public:/app/public:ro
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=16m
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- --eval
|
||||
- fetch('http://localhost:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
start_period: 5s
|
||||
retries: 3
|
||||
|
||||
directory-cli:
|
||||
# Keep this version matched to the web image if you pin it.
|
||||
image: ghcr.io/eleith/directory-cli:latest
|
||||
platform: linux/amd64
|
||||
profiles:
|
||||
- tools
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
volumes:
|
||||
- ./config:/app/config:ro
|
||||
read_only: true
|
||||
network_mode: none
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
+101
-412
@@ -1,448 +1,137 @@
|
||||
# Configuration contract
|
||||
# Configuration
|
||||
|
||||
The canonical [TypeBox schema](../packages/config/src/schema.ts) defines the
|
||||
configuration and inferred TypeScript types. The generated
|
||||
[editor JSON Schema](../packages/config/src/config.schema.json) describes authored
|
||||
optionality: fields with defaults can be omitted, but link titles/URLs and auth
|
||||
credentials remain required.
|
||||
your `config/directory.yaml` controls the behavior and appearance of your
|
||||
directory. craft it with care as yaml is powerful but unforgiving...
|
||||
|
||||
The shared loader and `directory validate` now read YAML, apply defaults, expand
|
||||
environment values and validate the result. The web server loads a complete
|
||||
immutable snapshot before serving, captures it once per request, and polls the
|
||||
configuration path for changes. Groups and bookmark titles render immediately,
|
||||
with plain `?link=<id>` details and external service links. Cards and details use
|
||||
prepared bookmark logos when no logo path is configured. Configured image files
|
||||
are served only for exact references in the captured snapshot. Omit auth for open access, or
|
||||
configure shared credentials or OIDC to protect the whole directory. Enabled authentication requires
|
||||
`AUTH_SECRET` and matching `ORIGIN`/`AUTH_URL` before the web server can start.
|
||||
## Config
|
||||
|
||||
## Migrating from Where
|
||||
Start with the [example](directory.example.yaml). The tables below follow the
|
||||
YAML headings; `credentials.username` means `username` inside `credentials`.
|
||||
Defaults apply when you leave a field out. Unknown fields and `null` are errors.
|
||||
|
||||
Directory was previously named Where. The package namespace is now `@directory/*`,
|
||||
the CLI executable is `directory`, the default filename is `config/directory.yaml`,
|
||||
and the public-directory override is `DIRECTORY_PUBLIC_DIR`. An explicit
|
||||
`CONFIG_PATH` can still point to an existing `config/where.yaml` file. Update
|
||||
operator environments manually; there is no automatic fallback or migration.
|
||||
The configuration format version and fields are unchanged.
|
||||
[version](#version) · [site](#site) · [appearance](#appearance) ·
|
||||
[links](#links) · [auth](#auth) · [Images](#images) · [Environment values](#environment-values)
|
||||
|
||||
## Start with the production starter
|
||||
### version
|
||||
|
||||
[directory.example.yml](../apps/web/config/directory.example.yml) is a minimal,
|
||||
credential-free production starter. It has an empty directory and open access,
|
||||
with no session secret required. **Configure authentication before publishing.**
|
||||
Its schema comment points to the checked-in artifact relative to `apps/web/config`:
|
||||
| Field | Value | Default |
|
||||
| --------- | ----------------------------- | ------- |
|
||||
| `version` | `1`. The file format version. | `1` |
|
||||
|
||||
### site
|
||||
|
||||
| Field | Value | Default |
|
||||
| ----------------------- | ------------------------------------------------------------------- | -------------- |
|
||||
| `title` | Nonblank text, e.g. `Home`. | `Directory` |
|
||||
| `description` | Text below the title. | Empty |
|
||||
| `footer` | Text at the bottom of the page. | Empty |
|
||||
| `logo_path` | [Image path](#images), e.g. `/public/house.svg`, or `/favicon.svg`. | `/favicon.svg` |
|
||||
| `favicon_path` | Browser tab icon. Same path options as `logo_path`. | `/favicon.svg` |
|
||||
| `open_links_in_new_tab` | `true` or `false`. Applies to every destination. | `true` |
|
||||
|
||||
### appearance
|
||||
|
||||
| Field | Value | Default |
|
||||
| --------------------- | ----------------------------------------------- | ----------- |
|
||||
| `primary_light_color` | Quoted hex color, e.g. `'#087'` or `'#008877'`. | `'#0f766e'` |
|
||||
| `primary_dark_color` | Quoted hex color. | `'#5eead4'` |
|
||||
|
||||
Quote the `#`, or YAML will helpfully turn your color into a comment.
|
||||
|
||||
### links
|
||||
|
||||
Required. Use `links: {}` for an empty page, or give each link an id (using only
|
||||
lowercase letters and hyphens if you must):
|
||||
|
||||
```yaml
|
||||
# yaml-language-server: $schema=../../../packages/config/src/config.schema.json
|
||||
version: 1
|
||||
links: {}
|
||||
links:
|
||||
lights:
|
||||
title: Lights
|
||||
url: http://homeassistant.local:8123
|
||||
group: Around the house
|
||||
tags: [lights, home]
|
||||
```
|
||||
|
||||
There is no hosted schema URL. If copying the file elsewhere, update the schema
|
||||
comment to the artifact's real location. The package also exports
|
||||
`@directory/config/schema.json`; runtime consumers resolve compiled ESM, not TS source.
|
||||
| Field | Value | Default |
|
||||
| --------------- | --------------------------------------------------- | ------------------ |
|
||||
| `title` | Nonblank name. | Required |
|
||||
| `url` | Full `http://` or `https://` address. | Required |
|
||||
| `description` | Plain-text notes; use YAML `\|` for multiple lines. | None |
|
||||
| `group` | Nonblank heading, e.g. `Around the house`. | Ungrouped |
|
||||
| `tags` | List of text labels, e.g. `[family, photos]`. | `[]` |
|
||||
| `logo_path` | [Image path](#images), e.g. `/public/lights.png`. | Generated initials |
|
||||
| `requires_auth` | `true` if the service needs sign-in. | `false` |
|
||||
| `internal_only` | `true` if intended for LAN/VPN access. | `false` |
|
||||
| `admin_only` | `true` if intended for administrators. | `false` |
|
||||
|
||||
For the separate populated, authenticated local review fixture and its image
|
||||
setup, use [demo/README.md](../demo/README.md). The isolated `demo/` directory is
|
||||
manual-only and excluded from builds and automated checks. It is never selected automatically at runtime.
|
||||
Native preview can use it directly without changing operator files; Docker
|
||||
review requires a deliberate manual copy. Back up existing active configuration
|
||||
before replacing it. Auth changes require a process restart, not the config poll.
|
||||
The three flags are labels only. They do not restrict access or hide links.
|
||||
|
||||
## Loading and validation
|
||||
### auth
|
||||
|
||||
```sh
|
||||
pnpm cli -- validate apps/web/config/directory.example.yml
|
||||
pnpm cli -- validate demo/config/directory.yaml
|
||||
```
|
||||
Omit this section to share your directory with the world.
|
||||
|
||||
The wrapper builds the shared package and standalone CLI first. The compiled
|
||||
command is `node apps/cli/dist/index.js validate [path]`; its executable is named
|
||||
`directory`. Gunshi defines commands and generates help, as in When. It accepts one
|
||||
leading `--` for package-manager forwarding. Use `directory --help` or
|
||||
`directory validate --help` for usage; help after a path also leaves the file unread.
|
||||
Use `credentials` for a basic username and password or `oidc` to turn it up to
|
||||
11 and wire into your authentication fiefdom. Choose one.
|
||||
|
||||
Path selection is explicit argument, then `CONFIG_PATH`, then
|
||||
`config/directory.yaml`, relative to the working directory. There is no production-
|
||||
specific path or search for nearby files. The web server pins the selected path
|
||||
at startup. `pnpm dev` runs from `apps/web`, so its default is
|
||||
`apps/web/config/directory.yaml`; an absolute `CONFIG_PATH` avoids working-directory
|
||||
ambiguity. The deployment layout is:
|
||||
| Field | Value | Default |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
||||
| `origin` | Your directory's public origin, e.g. `https://home.example.com`. No credentials, path, query, fragment, or trailing slash. | Required when auth is enabled |
|
||||
| `secret` | Optional random string of at least 32 characters. Keeps sessions across restarts. | Generated at each startup |
|
||||
| `credentials.username` | Shared username, e.g. `family`. | Required for credentials |
|
||||
| `credentials.password` | Password, or `'${DIRECTORY_PASSWORD}'`. | Required for credentials |
|
||||
| `oidc.issuer` | Your provider’s full HTTP(S) issuer URL. | Required for OIDC |
|
||||
| `oidc.client_id` | Client ID registered with your provider. | Required for OIDC |
|
||||
| `oidc.client_secret` | Secret, or `'${DIRECTORY_OIDC_CLIENT_SECRET}'`. | Required for OIDC |
|
||||
|
||||
```text
|
||||
deployment/
|
||||
config/directory.yaml
|
||||
public/
|
||||
```
|
||||
Use HTTPS, except for local testing at `http://localhost:3000` (or your dev port).
|
||||
OIDC's callback is your `origin` followed by `/auth/callback/oidc`.
|
||||
|
||||
The server selects the public directory once at startup through the shared
|
||||
`resolvePublicDir(configPath, { env, cwd })` helper. By default it uses `public`
|
||||
below the selected configuration path's grandparent, as in the layout above.
|
||||
`DIRECTORY_PUBLIC_DIR` overrides this location; relative overrides resolve from
|
||||
the working directory, not the YAML directory. The helper does not require the
|
||||
directory to exist. Environment changes require a restart.
|
||||
Validation checks image-path syntax only; neither loading nor snapshot preparation
|
||||
inspects image files or contacts any destination. The resolved filesystem root
|
||||
stays server-private.
|
||||
By default, restarting Directory signs everyone out. Set `secret` if you'd rather
|
||||
keep sessions; generate it once with `openssl rand -base64 32`. Changing it signs
|
||||
everyone out again. Ordinary config reloads keep the current session secret.
|
||||
|
||||
Files use one YAML 1.2 core document. Mapping keys are read as strings; duplicate
|
||||
keys, collection keys, unknown tags and other YAML versions are rejected. Merge
|
||||
keys are disabled. Alias expansion is bounded and cycles are rejected. Non-plain
|
||||
objects and unsafe keys are rejected before defaults can alter them. Arrays are
|
||||
accepted only where the schema allows lists, such as bookmark tags. Explicit nulls
|
||||
and wrongly typed values are errors, not requests for defaults. Unknown fields
|
||||
remain errors.
|
||||
Passwords and client secrets can go directly in YAML. Keep that file private.
|
||||
The [example](directory.example.yaml) has commented setups for both auth methods.
|
||||
Restart Directory after changing auth settings.
|
||||
|
||||
A valid file prints `Configuration is valid.` and exits 0. Invalid or unreadable
|
||||
input exits 1. Errors contain fixed messages, codes and field paths such as
|
||||
`/auth/credentials/password` or `/links/*/url`. Link IDs and unknown field names
|
||||
are replaced with `*`; filenames, values, environment-variable names, parser
|
||||
snippets and original exceptions are not printed.
|
||||
## Images
|
||||
|
||||
### File changes and last-good state
|
||||
Put `photos.png` in `public/` beside your Compose file and use
|
||||
`/public/photos.png` in YAML.
|
||||
|
||||
The web server polls inode, modification time and size approximately once per
|
||||
second. Mount the config directory rather than a single file so editor atomic
|
||||
rename saves remain visible. Invalid edits and deletion keep the previous
|
||||
snapshot; a later valid replacement recovers automatically. Ordinary requests do
|
||||
not read YAML or prepare snapshots. Reloads with the same loaded values and
|
||||
bookmark declaration order retain the existing snapshot and revision; comments
|
||||
and formatting alone do not create an update. `/healthz` stays generic and ready
|
||||
while a last-good snapshot is serving.
|
||||
|
||||
Open pages with JavaScript check the accepted revision every five seconds while
|
||||
visible, and when returning to the tab. A newer revision shows **Updates available**
|
||||
in a sticky bar above the header, with a **Refresh** link. It does not replace
|
||||
content or move focus automatically.
|
||||
Refresh reloads the current browser URL, including its search, so document colors
|
||||
and branding update together. Hidden/offline tabs pause checks, temporary failures
|
||||
back off, and authentication denials stop checks without exposing revision data.
|
||||
Without JavaScript, use the browser's normal refresh. Image-file byte changes alone
|
||||
do not create a YAML revision or an update notice.
|
||||
|
||||
An auth change rejects the entire replacement and reports `restart_required`,
|
||||
including changes that enable or disable auth. It never partially accepts the
|
||||
candidate's display changes or restarts the process automatically. Restore the
|
||||
original auth to resume display reloads. Providers and session policy are captured
|
||||
at startup; environment files are not polled.
|
||||
Use SVG, PNG, JPEG, GIF, or WebP, at most **5 MiB** each.
|
||||
|
||||
## Environment values
|
||||
|
||||
Interpolation happens in string values **after YAML parsing**, not in keys. Values
|
||||
from the environment are not parsed as YAML, expanded again, or converted to
|
||||
booleans/numbers. Injecting an environment map through the loader API avoids
|
||||
reading the process environment; the CLI normally uses its process environment.
|
||||
The config supports interpolation so you can keep secrets out of your config or
|
||||
just add a little dynamicness to your staticness.
|
||||
|
||||
| Syntax | Meaning |
|
||||
| ------------------ | ------------------------------------------------------------------------------- |
|
||||
| `${VAR}` | Use a set environment value; otherwise fail validation. |
|
||||
| `${VAR:-fallback}` | Use the fallback only when the variable is unset. An empty value counts as set. |
|
||||
| `$$` | A literal `$`; `$${VAR}` produces literal `${VAR}`. |
|
||||
This is optional. Create `.env` beside your Compose file with only the values
|
||||
you want to keep there:
|
||||
|
||||
Names use uppercase ASCII letters, digits and underscores, starting with a
|
||||
letter or underscore. Fallbacks are literal text ending at the first `}`. Other
|
||||
dollar text is unchanged. Quote templates in YAML when punctuation could affect
|
||||
YAML parsing.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
site:
|
||||
title: '${DIRECTORY_TITLE:-Home}'
|
||||
links:
|
||||
photos:
|
||||
title: Photos
|
||||
url: '${PHOTOS_URL:-https://photos.example.com}'
|
||||
tags: ['${PHOTO_TAG:-family photos}', backup]
|
||||
```dotenv
|
||||
DIRECTORY_PASSWORD=replace-with-your-own-password
|
||||
```
|
||||
|
||||
The editor schema accepts supported templates in constrained string fields;
|
||||
actual URLs, paths, colors and string choices are checked after expansion by the
|
||||
loader. Required fields remain required. Booleans and numbers must be authored
|
||||
with their actual YAML types.
|
||||
Then use `password: '${DIRECTORY_PASSWORD}'` in your YAML auth block.
|
||||
Compose passes the value to Directory. Keep `.env` private and out of Git.
|
||||
After editing it, run `docker compose up -d --force-recreate` to apply the change.
|
||||
|
||||
**Public text stays public.** Using an environment variable in a title,
|
||||
description, URL, tag or other display field does not make it secret. The future
|
||||
dashboard will expose those values. Authentication configuration and session
|
||||
secrets stay outside its display model. The validator prints neither kind.
|
||||
| Syntax | Meaning |
|
||||
| ------------------ | -------------------------------------------------- |
|
||||
| `${VAR}` | Required value; missing is an error. |
|
||||
| `${VAR:-fallback}` | Fallback when unset. An empty value counts as set. |
|
||||
| `$$` | Literal `$`. `$${VAR}` displays `${VAR}`. |
|
||||
|
||||
## Fields and defaults
|
||||
## Validation
|
||||
|
||||
| Field | Contract |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `version` | Only `1`; defaults to `1`. This is the file format, not the app release. |
|
||||
| `site` | Optional object. Defaults: title `Directory`, description/footer empty. |
|
||||
| `site.logo_path`, `site.favicon_path` | Independent site images, normalized by the shared loader to bundled `/favicon.svg` when omitted. Explicit `/favicon.svg` is also valid. Header and sign-in show the logo at 48px. Mounted selections publish bytes at `/logo` and `/favicon` independently. |
|
||||
| `appearance` | Optional object. `primary_light_color` defaults to `#0f766e`, `primary_dark_color` to `#5eead4`. Colors use three or six hex digits. |
|
||||
| `auth` | Omit for no auth; otherwise exactly one complete strategy below. No default strategy or credentials. |
|
||||
| `links` | Required keyed map; `{}` is an explicitly empty directory. Each value requires `title` and `url`; `tags` defaults to `[]` and the three advisory booleans default to `false`. |
|
||||
We all could use a little validation every now and then.
|
||||
|
||||
`site.open_links_in_new_tab` is a global boolean, defaulting to `true`. It controls
|
||||
only the external **Open** links in bookmark details. Use `false` to open services
|
||||
in the current tab:
|
||||
We publish a json schema to give you little red squigglies but you can also use
|
||||
our cli when you are in a pinch.
|
||||
|
||||
```yaml
|
||||
site:
|
||||
open_links_in_new_tab: false
|
||||
```
|
||||
|
||||
There is no per-bookmark override. Bookmark tiles still open details; Back links,
|
||||
search and other application navigation are unchanged. Native modifier-click,
|
||||
middle-click and browser context-menu actions remain available. The setting works
|
||||
with JavaScript enabled or disabled.
|
||||
|
||||
Appearance colors are public branding: they accent keyboard focus, the directory's
|
||||
Back-link underline and the filled sign-in action. Server-owned appearance emits
|
||||
only these validated colors and derived readable foregrounds as semantic CSS in
|
||||
the document head. The root layout sends only constant logo/favicon URIs. Page
|
||||
surfaces stay unchanged. Sign-in foregrounds meet 4.5:1 contrast against
|
||||
the configured primary; operators should still choose accessible focus accents.
|
||||
Without JavaScript, colors follow the OS scheme. Refresh the document after
|
||||
configuration reloads to update branding inputs.
|
||||
|
||||
Site copy and optional link `description` are plain strings. Description supports
|
||||
multiline guidance; there is no separate `good_to_know` field. Bookmarks have one
|
||||
optional `logo_path`; the site logo and favicon remain independent. Unknown structural fields are errors, not
|
||||
ignored extras; the old `site.group_by`, `sign_in` and `works_from` fields are not
|
||||
supported.
|
||||
|
||||
Link keys are lowercase letters/digits separated by single hyphens; a numeric-only
|
||||
key is not a slug. `constructor`, `prototype` and `__proto__` are forbidden keys.
|
||||
|
||||
### Tags, groups and service hints
|
||||
|
||||
```yaml
|
||||
tags: [photos, videos, backup]
|
||||
group: Photos & files
|
||||
requires_auth: true
|
||||
internal_only: false
|
||||
admin_only: false
|
||||
```
|
||||
|
||||
`tags` is a YAML list of strings, in flow form as above or block-list form. It
|
||||
defaults to `[]`. Scalar strings (including empty strings and CSV), maps, nulls
|
||||
and non-string elements are invalid. The loaded configuration retains each
|
||||
string's whitespace, case, duplicates and list order after environment expansion.
|
||||
Snapshot preparation trims each string and drops blank labels for display/search;
|
||||
it preserves order, duplicates and case. A comma inside an element is literal:
|
||||
`tags: ['photos, videos', backup]` has two tags, not three.
|
||||
|
||||
Environment interpolation is supported inside list elements. An environment value
|
||||
containing commas remains one tag; `tags: '${TAGS}'` is invalid even if the value
|
||||
looks like CSV or YAML. Environment values are never parsed into lists.
|
||||
|
||||
To migrate older configurations, replace each comma-separated `tags` scalar with
|
||||
an explicit YAML list, for example `tags: photos, videos, backup` becomes
|
||||
`tags: [photos, videos, backup]`. Use `tags: []` or omit the field for no tags.
|
||||
There is no automatic CSV compatibility or migration; update operator files
|
||||
manually.
|
||||
|
||||
`group` is an optional nonblank display label, independent of tags. If every link
|
||||
omits it, the planned directory is flat. If any link has a group, ungrouped links
|
||||
appear under Other. No category collection or global `group_by` selector is
|
||||
needed. Groups follow their first appearance in the YAML, and links within each
|
||||
group keep their declaration order. The ungrouped Other bucket stays last. A real
|
||||
group named Other remains distinct from that missing-group bucket. There is no
|
||||
sorting setting; remove `site.order` from older configurations.
|
||||
|
||||
The three booleans are **informational service hints only** and default to false:
|
||||
|
||||
- `requires_auth`: opening this service needs sign-in; not Directory's auth setting.
|
||||
- `internal_only`: intended for LAN/VPN access; no network detection or enforcement.
|
||||
- `admin_only`: intended for administrators; no role or visibility restriction.
|
||||
|
||||
False means no advisory badge, not a verified claim about the service. All
|
||||
visitors authorized to open Directory still see the same links. These flags neither
|
||||
hide links nor change the directory's actual `auth` strategy. Grouping,
|
||||
ordering and informational detail labels are rendered; search remains later work.
|
||||
All advisory flag combinations preserve the same link inclusion.
|
||||
|
||||
## Authentication shapes
|
||||
|
||||
Declare one, never both:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
credentials:
|
||||
username: family
|
||||
password: '${DIRECTORY_PASSWORD}'
|
||||
```
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
oidc:
|
||||
issuer: https://identity.example.com
|
||||
client_id: directory
|
||||
client_secret: '${DIRECTORY_OIDC_CLIENT_SECRET}'
|
||||
```
|
||||
|
||||
Username/password and all three OIDC fields are required and nonempty. Empty,
|
||||
null, unknown or mixed auth blocks are invalid; disabling the other strategy with
|
||||
`null` is invalid too. Omit `auth` entirely for no auth. Environment references are
|
||||
expanded before validation, including in OIDC issuer URLs. Editors recognize
|
||||
template syntax but cannot check the environment's values. Never put real
|
||||
credentials into committed examples.
|
||||
|
||||
### Sessions and deployment
|
||||
|
||||
Auth is all-or-nothing: a valid session opens the same directory for everyone.
|
||||
The service hints never become roles or per-link access rules. Sign-in and the
|
||||
required OIDC callback, generic health status and bundled resources are public;
|
||||
directory HTML, Kit data responses, generated bookmark logos and all mounted
|
||||
source URLs require a session, including the selected favicon's `/public/...`
|
||||
source. Image GET and HEAD requests return 401 before sign-in even with HTML
|
||||
Accept, rather than redirecting to an HTML sign-in page.
|
||||
The sign-in page exposes the site title, sign-in strategy, two primary colors,
|
||||
derived button foregrounds and constant logo/favicon URIs, not links, descriptions,
|
||||
sessions or private configuration. Dedicated public `/logo` and `/favicon` GET/HEAD
|
||||
endpoints publish only the selected site's logo and favicon bytes respectively,
|
||||
regardless of session. Selecting a bookmark/service image deliberately publishes
|
||||
those same bytes through the selected alias; its raw source URL stays protected.
|
||||
No configured source URI, filesystem path or image bytes enter public layout data.
|
||||
|
||||
When auth is enabled, set these server environment variables:
|
||||
|
||||
- `AUTH_SECRET`: a random string of at least 32 characters, generated with
|
||||
`openssl rand -base64 32`. It protects encrypted, seven-day JWT session cookies.
|
||||
- `ORIGIN`: the fixed public root origin, for example
|
||||
`https://directory.example.com`, with no path, query, credentials or trailing slash.
|
||||
Enabled auth uses HTTPS, with HTTP allowed only on `localhost` for local
|
||||
development, such as `http://localhost:5173`.
|
||||
- `AUTH_URL`: the same value as `ORIGIN`. This is Auth.js's standard URL setting;
|
||||
callbacks do not depend on client-supplied Host or forwarded headers. The proxy
|
||||
must still use the configured public origin.
|
||||
- `NEXTAUTH_URL`: leave unset, or match `ORIGIN` exactly.
|
||||
- `AUTH_REDIRECT_PROXY_URL`: unsupported; leave unset.
|
||||
|
||||
Invalid auth environment settings fail startup with `auth_failed` and a fixed
|
||||
hint naming the failing variable and its requirement. Diagnostics never include
|
||||
its value or raw error details. The standalone CLI does not check these web
|
||||
runtime settings.
|
||||
|
||||
No-auth mode requires none of these variables. Production Node does not automatically
|
||||
read `.env`; inject the environment through the process/container or Node's
|
||||
`--env-file`. Cookies are HTTP-only and SameSite=Lax, and are Secure for HTTPS
|
||||
origins. Protected/auth responses use private, no-store caching.
|
||||
|
||||
Register `${ORIGIN}/auth/callback/oidc` with your OIDC provider. Admission belongs
|
||||
to that provider's application assignments, not local accounts or role mappings.
|
||||
The app uses state, PKCE and nonce checks; do not turn them off for a provider.
|
||||
Shared credentials have no built-in rate limiting or lockout: use reverse-proxy
|
||||
throttling on sign-in or prefer OIDC for an internet-facing deployment.
|
||||
|
||||
Sign-out clears the browser's cookie. Restarting with the same `AUTH_SECRET` does
|
||||
not revoke existing sessions; rotate it when changing access policy must end
|
||||
existing access. No session database or revocation list is maintained. Loader/CLI
|
||||
validation checks configuration shape and interpolated values, not provider
|
||||
availability or these runtime environment prerequisites.
|
||||
|
||||
## URLs and images
|
||||
|
||||
Destinations and OIDC issuer URLs must be absolute HTTP(S), including local HTTP
|
||||
hosts/ports when appropriate. Relative, protocol-relative, executable, malformed
|
||||
or whitespace-containing URLs are rejected by the runtime schema.
|
||||
|
||||
Mounted images are decoded `/public/...` paths with SVG/PNG/JPEG/GIF/WebP extensions.
|
||||
Unicode/spaces and nested directories are allowed; empty/dot segments, percent
|
||||
escapes, backslashes, queries, fragments and control characters are not. This is
|
||||
syntactic validation, not filesystem access or MIME inspection. Site logo and favicon
|
||||
fields also accept the exact bundled `/favicon.svg` URI. The shared loader supplies
|
||||
that URI independently for omitted site fields; normalized site paths are required
|
||||
strings for both web and CLI consumers. `links.*.logo_path` stays optional and
|
||||
mounted-only, with no bundled URI selection. `links.*.favicon_path` is no longer
|
||||
supported; remove it from older configurations. The site favicon is unchanged.
|
||||
|
||||
`site.logo_path` renders a decorative, eager 48×48 image beside the directory title
|
||||
and description. Omission uses bundled `/favicon.svg` directly in server-rendered
|
||||
HTML, independently of `site.favicon_path`. With JavaScript, a failed configured
|
||||
image switches once to the bundle, including failures before hydration; a failed
|
||||
bundle stops without retrying. Without JavaScript, a broken configured image stays
|
||||
broken while the title and navigation remain usable. Changing the source URL
|
||||
allows a fresh primary attempt; repairing bytes at an unchanged URL requires a
|
||||
refresh. With auth enabled, the configured source URL requires a session.
|
||||
Sign-in uses public `/logo` for the same selection, with the same 48px/default/
|
||||
fallback behavior. The bundled mark is 🗂️ (card index dividers). `/logo` reads only
|
||||
the captured mounted `site.logo_path`, ignoring query selectors. A bundled selection
|
||||
(default or explicit) returns 404 before image I/O; missing or invalid mounted
|
||||
bytes return generic 404. A full document
|
||||
refresh accepts new selection/bytes. Raw sources and nonselected assets stay private.
|
||||
|
||||
`site.favicon_path` selects a source image, not the public alias. The shared layout
|
||||
maps validated mounted paths to `/favicon` and passes the resolved bundled URI
|
||||
through unchanged; it does not choose defaults or send mounted source URIs or
|
||||
private configuration. `/favicon` reads exactly the mounted source from the
|
||||
request's captured snapshot using the same bounded reader and GET/HEAD response
|
||||
headers below. A bundled selection (default or explicit) returns 404 before I/O. A missing, invalid or unavailable selected file also returns generic 404,
|
||||
without redirecting or falling back to the bundle. Valid configuration reloads
|
||||
change the selected bytes without changing the alias; invalid or auth-changing
|
||||
candidates retain the last good selection. No auth hook exceptions are involved.
|
||||
|
||||
The mounted-file endpoint serves only exact `assets.referencedPaths` from the request's
|
||||
captured snapshot: mounted site logo/favicon and bookmark logo paths, not bundled URIs or an
|
||||
arbitrary directory listing or static file tree. GET and HEAD use private,
|
||||
no-store and nosniff headers; HEAD has the same status/headers without a body.
|
||||
There are no range or conditional-cache responses. Missing, unreferenced,
|
||||
escaping, unreadable, unstable, oversized or unrecognized files receive a generic
|
||||
404 without filesystem details. A missing public directory is valid at startup.
|
||||
Accepted YAML reloads replace references; invalid or auth-changing candidates
|
||||
retain the last good set. Image bytes are read on request, so missing files and
|
||||
atomic image replacements recover without a YAML change.
|
||||
|
||||
Reads are bounded to 5 MiB per file. The reader checks canonical containment,
|
||||
regular-file type, the opened descriptor's Linux `/proc/self/fd` location and file
|
||||
stability. Symlinks resolving inside the selected root are allowed; escapes are
|
||||
not. File signatures must match the supported extension; this is not a full image
|
||||
decoder or XML validator. SVGs must be UTF-8 with an SVG root and no DTD/entity
|
||||
markup. They are **trusted operator content**, not sanitized uploads or a safe
|
||||
active-content boundary. Use only trusted files and do not expose the mounted
|
||||
directory directly through a proxy. Mounted-file serving needs Linux with procfs;
|
||||
without descriptor lookup it fails closed with 404, including in native Node.
|
||||
|
||||
### Prepared bookmark logos
|
||||
|
||||
The logo API at `/bookmarks/<id>/logo` currently returns a generated image, prepared
|
||||
server-side with the snapshot, not during page or image requests. Display links
|
||||
carry one resolved `logo_path`: the configured path when provided, otherwise this
|
||||
logo API URL. Cards and details render that value in the initial HTML. With
|
||||
JavaScript, a failed configured bookmark logo switches once to the same private
|
||||
logo API, including when the image failed before hydration. A failed fallback
|
||||
stops; an omitted primary already uses the API and is not retried on failure.
|
||||
Without JavaScript, valid configured and prepared logos still render, but a broken
|
||||
configured image cannot switch; text and navigation remain usable. Changing the
|
||||
primary URL or selected link allows a fresh primary attempt in the existing image.
|
||||
Repairing file bytes at the same URL does not trigger a browser retry: refresh the
|
||||
document to try again. No second image field or client-side generator is involved.
|
||||
|
||||
The current internal renderer uses DiceBear core 10.4.0 and a matching local
|
||||
Initials definition; no remote image service or browser generator is involved.
|
||||
Letters come from the title, falling back to its slug when the title has no usable
|
||||
letters. A stable group/link palette keeps the 64×64 images readable. The renderer
|
||||
can change without changing display fields or logo API URLs.
|
||||
|
||||
Bookmark logos use the directory's authentication policy, including GET and HEAD.
|
||||
A missing or removed bookmark gets a generic 404; responses are private/no-store
|
||||
and nosniff. Only the local URL is page data, never SVG bytes or private config.
|
||||
|
||||
## Maintaining the contract
|
||||
From the folder containing `compose.yaml`:
|
||||
|
||||
```sh
|
||||
pnpm schema:generate
|
||||
pnpm schema:check
|
||||
pnpm verify
|
||||
docker compose run --rm directory-cli config validate
|
||||
```
|
||||
|
||||
The generator compiles fresh canonical TS using the pinned compiler into a
|
||||
clean, disposable directory, then emits deterministic JSON with local `$defs`
|
||||
references. It walks maps/unions/nested schemas and removes only defaulted
|
||||
properties from `required`; it does not alter runtime types or literal annotation
|
||||
data. No generated TypeScript types or second runtime validator are maintained.
|
||||
|
||||
`schema:check` compares bytes without writing. It works before any build and is
|
||||
the first gate in local `verify` and both providers' config pipeline. Build does
|
||||
not regenerate the artifact or mask drift. Regenerate intentionally after schema
|
||||
changes, review the JSON diff, and update examples/tests together.
|
||||
|
||||
Browser-facing code may import **types only** from `@directory/config/types`. Its JS
|
||||
entry is empty and does not import the schema, format registry or loader.
|
||||
The canonical schema's TypeBox format registry entry is namespaced
|
||||
`directory-http-url`; the editor copy uses standard `uri` plus the HTTP(S) pattern.
|
||||
+20
-296
@@ -1,319 +1,43 @@
|
||||
# Docker and Compose
|
||||
# Deployment
|
||||
|
||||
Directory has one web container, following When's separate development and
|
||||
production Compose pattern. No host Node/pnpm installation is needed. Published
|
||||
images target Linux amd64; there is no worker, database or writable data volume.
|
||||
The UI renders bookmark groups and plain details with service links and prepared
|
||||
bookmark logos when no logo path is configured. With JavaScript, failed configured
|
||||
bookmark logos on cards and details switch once to their private prepared logo;
|
||||
failed fallbacks stop without retrying. Without JavaScript, broken configured
|
||||
images stay broken while text and links remain usable. After repairing bytes at
|
||||
the same URL, refresh the document to retry the primary image.
|
||||
The directory header renders `site.logo_path` at 48px, or bundled `/favicon.svg`
|
||||
when omitted. JavaScript switches a failed configured header image to the bundle
|
||||
once; without JavaScript, a broken configured image stays broken. Sign-in shows
|
||||
that selection through public `/logo`, with bundled 🗂️ as default/JavaScript
|
||||
fallback. Selecting `site.logo_path` deliberately publishes its bytes before login,
|
||||
even if also used by a bookmark. Its raw source URL stays private and is absent
|
||||
from public HTML and shared layout data. Server-generated head CSS uses only the
|
||||
two configured primary colors and derived readable button foregrounds. Root data
|
||||
contains only constant logo/favicon URIs; descriptions, links, auth/session data
|
||||
and filesystem paths stay out.
|
||||
Configured image files are now
|
||||
served only for exact snapshot references, with bounded 5 MiB reads and the
|
||||
directory's authentication policy for all mounted source URLs. Selecting
|
||||
`site.favicon_path` deliberately publishes that image's bytes at public `/favicon`,
|
||||
including before sign-in, even when the source is also a bookmark or site logo. Its raw
|
||||
`/public/...` URL stays protected. The shared layout sends only the constant alias,
|
||||
not the source URI. Omission uses bundled `/favicon.svg`; a missing or invalid
|
||||
selected file returns 404, without a bundled fallback.
|
||||
use docker compose with our [compose](compose.yaml)
|
||||
|
||||
## Local production review
|
||||
i mean, there are many other ways, but i'm not going to talk about them.
|
||||
|
||||
From the repository root, prepare files in the web directory so Compose reads
|
||||
the adjacent `.env`. These commands create only missing config/env files and
|
||||
preserve any existing operator configuration:
|
||||
Fill out `config/directory.yaml`. The [example](directory.example.yaml) has comments
|
||||
for sign-in and SSO. See [configuration](config.md) for all the juicy options.
|
||||
|
||||
## start up
|
||||
|
||||
```sh
|
||||
cd apps/web
|
||||
test -e .env || cp -n .env.example .env
|
||||
test -e config/directory.yaml || cp -n config/directory.example.yml config/directory.yaml
|
||||
mkdir -p public
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The production starter is empty and has **open access**, with no session secret
|
||||
required. Enable your own [authentication](#authentication-and-https) before
|
||||
publishing. Existing active YAML remains unchanged and may have different auth
|
||||
requirements. If auth is enabled, set a unique random `AUTH_SECRET` of at least
|
||||
32 characters and a matching origin before starting; there is no fallback.
|
||||
you can edit most configurations without having to restart
|
||||
|
||||
For the populated local-only review instead, follow [demo/README.md](../demo/README.md).
|
||||
It documents explicit configuration replacement after backup, non-overwriting
|
||||
image copies into the runtime mount, and the required secret. Its preferred
|
||||
native preview leaves operator configuration and media untouched. Demo files
|
||||
are excluded from the Docker build context, not copied into the image.
|
||||
## inspect logs
|
||||
|
||||
Once the configuration choice and any auth settings are ready, still in `apps/web`:
|
||||
not sure what is going on? check out the logs
|
||||
|
||||
```sh
|
||||
docker compose up --build --pull never
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
Open **http://localhost:3000**, not an HTTP IP address when auth is enabled.
|
||||
`/healthz` returns generic readiness. Stop with Ctrl-C; `docker compose down`
|
||||
removes the container, not your mounted files. Add `-d` to run in the background
|
||||
and use `docker compose logs -f` for diagnostics.
|
||||
and visit `http://localhost:3000/healthz` to see what is up.
|
||||
|
||||
Edit your ignored `config/directory.yaml`, never the tracked starter or demo, to try
|
||||
site-copy reloads. The **directory** mount keeps atomic rename saves visible.
|
||||
Invalid later YAML keeps the last valid snapshot; invalid initial YAML prevents
|
||||
startup. Auth changes require a process restart, not just a polling interval.
|
||||
See the [configuration contract](config.md).
|
||||
or down.
|
||||
|
||||
The production service runs as UID/GID **1000:1000**, with a read-only root
|
||||
filesystem, temporary `/tmp`, and read-only config/public mounts. Ensure this
|
||||
identity can read the mounted files and traverse their directories. Do not fix
|
||||
permissions with a recursive world-writable change. Nothing writes to your
|
||||
configuration or assets. The `public` mount supplies the protected image endpoint;
|
||||
**do not serve it directly through a proxy**. Only trusted operator SVGs belong
|
||||
here: image recognition is not sanitization or a full decoder. Missing directories
|
||||
or files do not prevent startup; unavailable images return generic 404s.
|
||||
|
||||
Mounted-file reads require Linux `/proc/self/fd` access to validate the opened
|
||||
inode. This applies to the container and native Node; missing procfs fails closed
|
||||
with 404. The root is resolved once at boot, by default as `public` below the
|
||||
selected config path's grandparent (`/app/public` in production Compose).
|
||||
`DIRECTORY_PUBLIC_DIR` can override it; relative values use the process working
|
||||
directory. This selects the root, not permission to serve arbitrary files under
|
||||
it. See [image behavior](config.md#urls-and-images).
|
||||
|
||||
## Authentication and HTTPS
|
||||
|
||||
The production starter has no auth. Before publishing, configure your own
|
||||
[credentials or OIDC strategy](config.md#authentication-shapes) in the mounted YAML;
|
||||
never use the isolated demo's known credentials for a deployment.
|
||||
Set a unique generated session secret and any custom YAML environment references in
|
||||
`.env`; generate `AUTH_SECRET` with:
|
||||
## restart
|
||||
|
||||
```sh
|
||||
openssl rand -base64 32
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
For example, shared credentials use:
|
||||
## update
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
credentials:
|
||||
username: family
|
||||
password: '${DIRECTORY_PASSWORD}'
|
||||
```
|
||||
|
||||
This custom YAML needs `DIRECTORY_PASSWORD` in `.env`, in addition to `AUTH_SECRET`.
|
||||
Only YAML that references it needs that variable. Do not put real credentials
|
||||
in the image, build arguments, tracked examples or shell history. Compose injects
|
||||
`.env` at runtime; YAML references are expanded by Directory, not Compose.
|
||||
|
||||
For your production HTTPS proxy, also set the canonical public root origin:
|
||||
|
||||
```dotenv
|
||||
ORIGIN=https://directory.example.com
|
||||
```
|
||||
|
||||
Compose sets **both `ORIGIN` and `AUTH_URL`** to this value. Do not include a
|
||||
trailing slash or path. Without an override, local production review uses
|
||||
`http://localhost:3000`. If you change `DIRECTORY_PORT`, set `ORIGIN` to the URL
|
||||
you actually open. Production Node trusts that fixed origin rather than
|
||||
client-supplied forwarding headers. Invalid auth environment settings stop
|
||||
startup with `auth_failed` and a fixed, value-free hint naming the setting to fix.
|
||||
|
||||
The host binding defaults to `127.0.0.1`. A host reverse proxy can reach port
|
||||
3000; a containerized proxy needs an operator-configured shared network, or an
|
||||
appropriately restricted host binding through `DIRECTORY_BIND`. Keep direct
|
||||
access restricted to your proxy/firewall. The image itself listens on
|
||||
`0.0.0.0:3000` and does not terminate TLS. Root-path hosting only; subpaths are
|
||||
not supported. Shared credentials need proxy throttling. OIDC also needs the
|
||||
registered `${ORIGIN}/auth/callback/oidc` callback and provider admission rules.
|
||||
|
||||
After changing `.env`, recreate rather than merely restart the existing container:
|
||||
if a new version is released, update your compose with the latest version and
|
||||
then
|
||||
|
||||
```sh
|
||||
docker compose up -d --no-build --force-recreate
|
||||
docker compose up -d --force-recreate
|
||||
```
|
||||
|
||||
For YAML-only auth changes, `docker compose restart` reloads the boot policy.
|
||||
Keep `AUTH_SECRET` stable to preserve sessions; rotate it to revoke them. Actual
|
||||
HTTPS/cookie/proxy and external-provider behavior still need deployment testing.
|
||||
|
||||
## Development container
|
||||
|
||||
First complete the configuration choice and, if enabling auth, the stable
|
||||
`AUTH_SECRET` setup above. Use the same `.env`, YAML and public directory. If your host identity is not
|
||||
1000:1000, set `LOCAL_UID`/`LOCAL_GID` in `.env` using `id -u` and `id -g`.
|
||||
These settings control both image-generated directory ownership and the running
|
||||
user. Rebuild after changing them.
|
||||
|
||||
```sh
|
||||
ORIGIN=http://localhost:5173 docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
With mise and fnox installed, `ORIGIN=http://localhost:5173 mise docker up --build`
|
||||
is an optional shortcut from this directory. It uses `docker-compose.dev.yml`.
|
||||
The `*.local.toml` files stay outside Git, formatting and the Docker build context.
|
||||
|
||||
The shared fnox config has no session-secret default. If auth is enabled, supply
|
||||
a random `AUTH_SECRET` through your local fnox provider, shell or `.env`. No-auth
|
||||
configurations need no secret. If your existing `mise docker` workflow supplies
|
||||
that secret through fnox, keep using it. Compose passes that value into the container; the app still
|
||||
rejects missing or short secrets when auth is enabled. Other YAML environment
|
||||
references remain configured in `.env`.
|
||||
|
||||
Open **http://localhost:5173**. The explicit origin in the direct command above
|
||||
overrides a production HTTPS origin in `.env` for this local dev session. Web source/static/config binds are
|
||||
read-only to the container; Vite caches and generated files stay inside it.
|
||||
Host source edits trigger Vite reloads. Config YAML is polled as in production.
|
||||
Shared config TypeScript is compiled at container startup; restart the dev
|
||||
container after changing that source. Dependency/lockfile changes require a
|
||||
rebuild. There is no separate compiler watcher or source-export alias.
|
||||
|
||||
Validate the mounted YAML with the existing CLI inside the dev image:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.dev.yml run --build --rm directory-app \
|
||||
node /m/apps/cli/dist/index.js validate /m/apps/web/config/directory.yaml
|
||||
```
|
||||
|
||||
The dev image includes the standalone CLI; the production image does not. It
|
||||
checks YAML/interpolation, not web runtime prerequisites or provider reachability. To stop this setup:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
```
|
||||
|
||||
Dev and production use the same Compose project/service name; stop one before
|
||||
switching to the other. Dev is a local source-preview server, not the recommended
|
||||
public HTTPS deployment.
|
||||
|
||||
## Published production images
|
||||
|
||||
Successful main builds publish `latest` to:
|
||||
|
||||
- `git.eleith.com/eleith.private/directory:latest`
|
||||
- `ghcr.io/eleith/directory:latest`
|
||||
|
||||
Manually authorized stable web tags publish versioned images to the same two
|
||||
repositories. For example, `@directory/web@1.2.3` creates `:1.2.3`, `:1.2` and
|
||||
`:1` aliases, never `:latest`. The tag must be a complete stable version with no
|
||||
leading zeroes and exactly match the web manifest; validation happens before
|
||||
registry login/publication. CLI tags do not publish another image. See the
|
||||
[manual release workflow](development.md#manual-app-releases) for Changesets,
|
||||
version review and separately authorized tag pushes.
|
||||
|
||||
Image availability and registry read permissions are operator setup. Before the
|
||||
first successful publishing run, build locally as above. Afterwards, with `.env`
|
||||
and mounts already prepared:
|
||||
|
||||
```sh
|
||||
docker login git.eleith.com
|
||||
docker compose pull
|
||||
docker compose up -d --no-build
|
||||
```
|
||||
|
||||
To choose GHCR, set `DIRECTORY_IMAGE=ghcr.io/eleith/directory:latest` in `.env`
|
||||
and authenticate there if required. To select a published release instead, set
|
||||
`DIRECTORY_IMAGE=git.eleith.com/eleith.private/directory:1.2.3` (or the GHCR
|
||||
equivalent); use a real published version, not this illustrative value. Minor
|
||||
and major aliases move with releases; use a full version or digest for pinning.
|
||||
Pulling does not apply an update until you recreate the container. There is no
|
||||
automatic homelab deployment or separately published CLI image.
|
||||
|
||||
Both providers keep PRs non-publishing. Main and stable web-tag publication wait
|
||||
for web checks/E2E and explicit schema/config/CLI gates. On Woodpecker, authorize
|
||||
`gitea_docker_registry` server-side for `push` and `tag` events and the exact
|
||||
`woodpeckerci/plugin-docker-buildx:6.0.3` image, never PRs. There is no native
|
||||
branch filter for secrets; workflow conditions do not replace trusting push
|
||||
writers or stronger secret-backend policy. SMTP stays main-push-only, unchanged
|
||||
by release tags. Confirm provider permissions and actual runs as described in
|
||||
[development](development.md#main-and-release-image-publication).
|
||||
|
||||
The Docker build copies compiled output and pruned production dependencies into
|
||||
the non-root runtime image. Operator
|
||||
YAML, secrets, mounted public files and the isolated `demo/` directory are excluded
|
||||
from the build context.
|
||||
The runtime contains no application source, build toolchain or browser installation.
|
||||
Final container behavior, mounts and image size are owner-run manual checks.
|
||||
|
||||
## Owner Docker review checklist
|
||||
|
||||
These checks require your Docker installation; source/Node checks alone are not
|
||||
a Docker pass. Use disposable copies of any real configuration.
|
||||
|
||||
1. Run `docker compose config --quiet` and the equivalent with
|
||||
`-f docker-compose.dev.yml`. Build/start each setup above; confirm health,
|
||||
configured copy, logs, Ctrl-C and restart. Try source reloads in dev and ensure
|
||||
no root-owned files appear in the host checkout.
|
||||
2. After the production build, check the **actual image**, not compressed download
|
||||
size (adjust the image name if overridden):
|
||||
|
||||
```sh
|
||||
docker image inspect git.eleith.com/eleith.private/directory:latest \
|
||||
--format '{{.Size}} bytes; user={{.Config.User}}; arch={{.Architecture}}'
|
||||
docker compose exec directory-app id
|
||||
```
|
||||
|
||||
Require **less than 400000000 bytes**, non-root UID/GID 1000 and amd64 before
|
||||
approving publication. Inspect that the final image contains only runtime
|
||||
output/dependencies, not source, compiler, browser, fixtures or operator files.
|
||||
Check the effective root filesystem and bind-mount settings too:
|
||||
|
||||
```sh
|
||||
docker inspect "$(docker compose ps -q directory-app)" \
|
||||
--format 'read-only-root={{.HostConfig.ReadonlyRootfs}}{{range .Mounts}}; {{.Destination}} writable={{.RW}}{{end}}'
|
||||
docker compose exec directory-app node --eval \
|
||||
"require('node:fs').openSync('/app/config/directory.yaml','r+')"
|
||||
```
|
||||
|
||||
Require `read-only-root=true` and `writable=false` for `/app/config` and
|
||||
`/app/public`. The write-open command must fail; it requests write access
|
||||
without changing file bytes. Only `/tmp` is intended to be writable.
|
||||
|
||||
3. Edit site copy, save by atomic rename, then try invalid YAML and deletion/
|
||||
restoration. Last-good copy and generic health must survive later failures.
|
||||
With the container stopped, invalid YAML or enabled auth with a missing secret
|
||||
or invalid origin must fail closed on startup. Restore valid settings afterwards.
|
||||
4. In credentials mode, try wrong/right passwords, JavaScript disabled, sign-out,
|
||||
and a private `__data.json` URL without a session. Only the signed-in session
|
||||
should reach bookmark cards, details and directory data. Click a card to its
|
||||
real `?link=<id>` details, use the separate new-tab service link and return with
|
||||
Back to directory. Try JavaScript disabled. Changing auth must require a
|
||||
restart, not silently alter a running session policy.
|
||||
5. In a disposable public mount, configure a bookmark's `logo_path` to a small
|
||||
trusted SVG. With JavaScript disabled, check its card and details render after
|
||||
sign-in. Without a session, GET/HEAD requests to both mounted files and
|
||||
`/bookmarks/<id>/logo` must return 401 even with `Accept: text/html`; configured
|
||||
favicon source URLs stay private too. Before sign-in, check the tab uses public
|
||||
`/favicon` for a selected site favicon. Selected `site.logo_path` bytes are also
|
||||
deliberately public through `/logo`, never through the raw source URL. Check
|
||||
sign-in's 48px logo/default 🗂️ and one-time JavaScript fallback; omission or
|
||||
unavailable selections return generic 404 from `/logo`.
|
||||
Omit the selection to use bundled `/favicon.svg` with a 404 at `/favicon`.
|
||||
Missing or invalid selected files must return 404 rather than a static fallback.
|
||||
Check private/no-store and nosniff headers,
|
||||
bodyless HEAD, unreferenced and escaping-symlink 404s, missing-file recovery,
|
||||
atomic image replacement, and valid/invalid YAML reference reloads. Omit
|
||||
`logo_path` to check the prepared logo remains usable. With JavaScript enabled,
|
||||
block a configured bookmark image in devtools and check cards/details use the
|
||||
prepared logo once; block the fallback too and check there is no retry loop.
|
||||
Restore access and refresh to retry the primary. Verify the actual container's
|
||||
procfs descriptor lookup as its non-root runtime user:
|
||||
|
||||
```sh
|
||||
docker compose exec directory-app node --input-type=module --eval \
|
||||
"import {open,realpath} from 'node:fs/promises'; const f=await open('/app/config/directory.yaml'); try { await realpath('/proc/self/fd/'+f.fd); console.log('procfs descriptor lookup available'); } finally { await f.close(); }"
|
||||
```
|
||||
|
||||
Do not replace this owner-run image/mount check with a host-only Node result.
|
||||
|
||||
6. In the normal HTTPS/proxy setup, verify redirects, Secure cookies, CSRF,
|
||||
configured OIDC callback and proxy throttling. Rotate the session secret and
|
||||
confirm old sessions stop working. Do not treat local HTTP smoke or a fake
|
||||
issuer as a verified external-provider deployment.
|
||||
+12
-873
@@ -1,885 +1,24 @@
|
||||
# Development
|
||||
|
||||
## Current scope
|
||||
Read the [philosophy](philosophy.md).
|
||||
|
||||
The bootstrap contains all three workspaces, local quality gates, and per-package
|
||||
Woodpecker/GitHub workflow definitions. CLI help and the SSR directory read the
|
||||
same supported configuration format version. The TypeBox schema, inferred types
|
||||
and deterministic editor-schema generation are implemented, along with YAML
|
||||
loading, environment interpolation and standalone CLI validation; see
|
||||
[configuration](config.md). The web server now loads before serving, polls the
|
||||
configuration path, and captures one immutable snapshot per request. The shell
|
||||
renders configured groups, bookmark titles and SSR details with external service
|
||||
links. Details progressively enhance into a responsive dialog. Cards and details use server-prepared bookmark logos when no logo path is
|
||||
configured. Configured image files now serve through exact captured snapshot
|
||||
references, not an arbitrary static directory. Optional credentials or OIDC authentication gates the entire directory;
|
||||
no-auth mode stays open and requires no session secret. [Docker and Compose](deployment.md)
|
||||
provide dev/production containers, main `latest` images and manually tagged
|
||||
stable releases.
|
||||
Then resist the urge to add a plugin system.
|
||||
|
||||
## Toolchain
|
||||
|
||||
`mise.toml` pins Node **26.5.0** and pnpm **11.9.0**. The shared catalog pins direct
|
||||
dependencies exactly, including:
|
||||
|
||||
| Tool | Version |
|
||||
| --------------------------------- | --------------- |
|
||||
| SvelteKit / Svelte | 2.70.1 / 5.56.0 |
|
||||
| adapter-node / Svelte Vite plugin | 5.5.4 / 7.1.2 |
|
||||
| Vite / TypeScript | 8.0.16 / 6.0.3 |
|
||||
| ESLint / typescript-eslint | 10.4.1 / 8.60.0 |
|
||||
| eslint-plugin-svelte | 3.18.0 |
|
||||
| Prettier / prettier-plugin-svelte | 3.8.3 / 3.5.2 |
|
||||
| Vitest / coverage-v8 | 4.1.7 / 4.1.7 |
|
||||
| Playwright | 1.61.1 |
|
||||
| Bits UI | 2.18.1 |
|
||||
| Fuse.js (full build) | 7.5.0 |
|
||||
| unplugin-icons / Phosphor JSON | 23.0.1 / 1.2.2 |
|
||||
| Changesets CLI | 3.0.1 |
|
||||
|
||||
Published peer ranges support this combination. `@types/node` 24.12.4 satisfies
|
||||
Vite/Vitest's peer ranges; it does not describe every Node 26 API. The base ESLint
|
||||
configuration now loads Svelte template rules and the actual web configuration.
|
||||
Do not use SvelteKit 3 configuration APIs with this pinned Kit 2 stack.
|
||||
UI glyphs use When's `virtual:icons/ph/*` imports with unplugin-icons' Svelte
|
||||
compiler. Only selected SVGs are compiled into the app; there is no browser icon
|
||||
API, font or runtime collection. Branding and bookmark/service images remain separate.
|
||||
|
||||
## Setup and commands
|
||||
With Docker, mise, and fnox installed, run from `apps/web`:
|
||||
|
||||
```sh
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm --filter @directory/web exec playwright install chromium
|
||||
pnpm verify
|
||||
mkdir -p config
|
||||
cp -n ../../docs/directory.example.yaml config/directory.yaml
|
||||
mise docker up
|
||||
```
|
||||
|
||||
On Linux, Playwright may also need system libraries. On a machine where you
|
||||
control system package installation, its `install --with-deps chromium` command
|
||||
installs them; do not use it to bypass sandbox restrictions. Install the Chromium
|
||||
revision matching the pinned Playwright version after changing that dependency.
|
||||
if it works. celebrate.
|
||||
|
||||
| Command | What it does |
|
||||
| -------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `pnpm build` | Builds config, then CLI and the adapter-node production server. |
|
||||
| `pnpm check` | Checks config/CLI TypeScript plus web Svelte/TypeScript diagnostics. |
|
||||
| `pnpm lint` / `pnpm format` | Checks or writes root/workspace Prettier formatting; lint also runs ESLint. |
|
||||
| `pnpm test` / `pnpm test:coverage` | Runs all workspace unit/artifact tests, with diagnostic coverage. |
|
||||
| `pnpm test:e2e` | Runs Chromium against the existing production build. |
|
||||
| `pnpm schema:generate` / `pnpm schema:check` | Generates editor JSON / checks drift without writing, before any build. |
|
||||
| `pnpm verify` | Checks schema drift, builds, checks types/lint, runs coverage tests, then E2E. |
|
||||
| `pnpm dev` | Builds config, then starts Vite on `127.0.0.1:5173`. |
|
||||
when you come back, in a browser, navigate to http://localhost:5173
|
||||
|
||||
`pnpm build:packages` rebuilds just shared output. Package exports deliberately
|
||||
resolve `dist/index.js` and `dist/index.d.ts`, with no source aliases or custom
|
||||
loader. `@directory/config/types` is a type-only entry; the schema JSON is exported
|
||||
separately. Schema generation compiles fresh source into disposable output and
|
||||
never relies on an existing `dist`. Build before independent workspace
|
||||
checks/tests; `pnpm verify` does this in order.
|
||||
After editing config source during development, rebuild shared output to make it
|
||||
visible to Vite; there is no automatic config-source compiler/watch process.
|
||||
The config package pins YAML **2.9.1**. The CLI uses **Gunshi 0.35.1** for
|
||||
commands, arguments and generated help, matching When. It uses catalog-pinned esbuild
|
||||
**0.28.2** for its Node bundle; dependency install scripts are disabled for
|
||||
esbuild because the platform binary ships as an optional dependency.
|
||||
## write code
|
||||
|
||||
The web server needs configuration at runtime, not at build time. From the
|
||||
repository root, preview the isolated demo without creating or modifying
|
||||
operator configuration or copying images:
|
||||
the app is a svelte app and in development it is hot reloading. code away and
|
||||
watch the magic work (or break).
|
||||
|
||||
```sh
|
||||
CONFIG_PATH="$PWD/demo/config/directory.yaml" \
|
||||
AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
ORIGIN=http://localhost:5173 AUTH_URL=http://localhost:5173 pnpm dev
|
||||
```
|
||||
|
||||
Open **http://localhost:5173**. See [the demo quickstart](../demo/README.md) for
|
||||
local-only credentials, manual preview scope and production preview. Its 32 bookmarks
|
||||
span five uneven groups and use generated default logos; site branding uses the
|
||||
bundled default, so no image mounts or asset copies are needed. This command uses
|
||||
real authentication with a fresh ephemeral secret, not an auth bypass.
|
||||
The separate `apps/web/config/directory.example.yml` starter is empty and open,
|
||||
requires no session secret, and must have authentication added before publishing.
|
||||
|
||||
`pnpm dev` runs Vite from `apps/web`, so its default path is
|
||||
`apps/web/config/directory.yaml`. Absolute `CONFIG_PATH` and `DIRECTORY_PUBLIC_DIR`
|
||||
values avoid working-directory ambiguity. An existing active configuration stays
|
||||
unchanged with the command above. Back it up yourself before explicitly choosing
|
||||
to replace it with a demo copy; setup must never automatically overwrite it.
|
||||
Auth changes require a process restart, not just waiting for the file poll.
|
||||
Use HTTPS or `http://localhost:5173`, never an HTTP IP address with auth enabled;
|
||||
update both `ORIGIN` and `AUTH_URL` if the port changes. See
|
||||
[authentication setup](config.md#authentication-shapes) and
|
||||
[Compose setup](deployment.md#local-production-review) for stable secrets and
|
||||
manual, non-overwriting asset copying.
|
||||
|
||||
### CLI validation
|
||||
|
||||
```sh
|
||||
pnpm cli -- --help
|
||||
pnpm cli -- validate apps/web/config/directory.example.yml
|
||||
pnpm cli -- validate demo/config/directory.yaml
|
||||
```
|
||||
|
||||
The root command builds shared config and the CLI, then runs its compiled Node
|
||||
entrypoint. The wrapper also emits build logs; the following stream guarantees
|
||||
refer to the compiled CLI itself:
|
||||
|
||||
- Help returns 0 on stdout without loading configuration.
|
||||
- `directory validate [path]` returns 0 on stdout for a valid file, otherwise 1 on
|
||||
stderr. It checks the actual environment, with no weaker structure-only mode.
|
||||
- Missing commands, unknown options and extra paths return 1 on stderr. Exactly
|
||||
one leading `--` is accepted for package-manager forwarding; repeated or
|
||||
mid-command separators are rejected. Gunshi handles help in its normal flag
|
||||
positions, including after a path, without loading configuration.
|
||||
- Output contains no configuration values, filenames or parser snippets. See
|
||||
[configuration](config.md) for paths, interpolation and safe error codes.
|
||||
|
||||
The package's build command runs TypeScript and esbuild directly, following
|
||||
When's pattern. It cleans `dist`, emits declarations, and bundles `index.js` and
|
||||
the side-effect-free `cli.js`. Only Node built-ins remain external. The
|
||||
executable keeps its shebang and executable mode; it needs no source, workspace
|
||||
resolution or `node_modules` at runtime. `runCli` is async and accepts injected
|
||||
output sinks and an optional environment map.
|
||||
|
||||
Artifact tests copy only those two bundles outside the workspace, run from that
|
||||
temporary directory, and compare acceptance/errors with the shared loader. They
|
||||
cover configuration paths, auth/environment failures, secret sentinels, help,
|
||||
arguments and API import without executing main. Config compiled-module and CLI
|
||||
subprocess tests do not contribute to source coverage; passing them is not a
|
||||
claim of full source coverage.
|
||||
|
||||
### Prepared configuration snapshots
|
||||
|
||||
`prepareSnapshot` in `apps/web/src/lib/server/config/snapshot.ts` clones the
|
||||
validated private configuration, creates the display model and exact referenced
|
||||
asset lists and bookmark logos, then freezes the complete snapshot.
|
||||
`getBookmarkLogos` in `bookmark-logos.ts` currently renders SVGs with the internal
|
||||
Initials style. SVG strings stay in the private, frozen `assets.bookmarkLogos` map
|
||||
for every link, including those with configured logos. Display links carry one
|
||||
required `logo_path`, resolved to the configured path or `/bookmarks/<id>/logo`.
|
||||
This logo API remains available for every bookmark. `BookmarkLogo.svelte` is a
|
||||
thin domain wrapper: it passes the resolved primary and derives the same private
|
||||
API fallback for the shared `Logo.svelte`, at 64px on cards and details. The
|
||||
protected header also uses `Logo`, at 48px with eager loading, `site.logo_path`
|
||||
and bundled `/favicon.svg` as fallback. The shared config loader resolves omitted
|
||||
site logo/favicon paths to `/favicon.svg` for both web and CLI; normalized site and
|
||||
display paths are required strings. `getSite` selects them directly, without
|
||||
choosing defaults. Bundled paths are excluded from mounted asset references.
|
||||
Sign-in uses the selected bytes through public `/logo` at 48px, with bundled 🗂️ as
|
||||
default and one-time JavaScript fallback. No configured source URI enters public
|
||||
HTML or shared layout data. The site favicon remains independent.
|
||||
|
||||
`Logo` makes one JavaScript fallback attempt. Its mount check catches image failures
|
||||
before hydration; fallback failures stop, including when primary and fallback
|
||||
are the same. Changing the source/fallback pair resets the attempt in place,
|
||||
including revisiting a previously failed source. The bookmark fallback encodes
|
||||
link identity, so switching links also resets the attempt. Repairing bytes at an
|
||||
unchanged URL requires a document refresh, not automatic polling. Without
|
||||
JavaScript, broken configured images stay broken; text and links remain usable.
|
||||
Configured paths still reach the mounted-image endpoint.
|
||||
The directory page sends only the snapshot's `display` member; the root separately
|
||||
publishes allowlisted branding inputs described below. The client-facing interfaces
|
||||
in `src/lib/display/types.ts` contain no server imports. Snapshot IDs are fresh
|
||||
random UUIDs, not hashes of configuration or credentials.
|
||||
|
||||
`prepareDirectory` builds the browser data through `getSite(config)` and
|
||||
`getGroups(config)`. These helpers select public fields, trim each YAML-list tag
|
||||
and drop blank labels, and group links in YAML declaration order. Tag order,
|
||||
duplicates and case are preserved; commas inside a tag remain literal. Named
|
||||
groups follow their first appearance, with ungrouped links last. Group identities
|
||||
keep a configured Other separate from the missing-group bucket.
|
||||
|
||||
`loadDirectory(path?, { env, report }?)` in
|
||||
`apps/web/src/lib/server/config/load-directory.ts` loads and prepares the first
|
||||
snapshot before returning a directory object:
|
||||
|
||||
```ts
|
||||
const directory = await loadDirectory(path, { report });
|
||||
const captured = directory.currentSnapshot();
|
||||
await directory.reload();
|
||||
directory.stop();
|
||||
```
|
||||
|
||||
- `currentSnapshot()` returns a complete immutable snapshot. Replacements change
|
||||
one private reference; earlier captures keep their complete version.
|
||||
- `reload()` runs one attempt at a time. Requests during loading or preparation
|
||||
coalesce into one follow-up. Identical loaded values and bookmark declaration
|
||||
order keep the current snapshot and revision without preparing logos again.
|
||||
Load/preparation failures retain the current
|
||||
snapshot and report a safe issue. Auth differences from the initial snapshot
|
||||
report `restart_required` and reject the entire candidate, including display
|
||||
edits. Restoring the original auth permits later display reloads.
|
||||
- `stop()` is terminal. It clears pending work and prevents later publication or
|
||||
reporting, without claiming to cancel I/O already in progress.
|
||||
|
||||
Startup failure rejects with a redacted `DirectoryError`; no directory object is
|
||||
returned. There is no separate `start()` or public publisher. The config package
|
||||
only loads and validates configuration.
|
||||
|
||||
### Web startup and file polling
|
||||
|
||||
The Kit `init` hook skips build analysis, then chooses separate production or
|
||||
development startup functions. Both use the shared `bootApp` function; production
|
||||
startup finishes before adapter-node opens a socket. Compilation needs no mounted
|
||||
YAML or credentials. Startup errors stay fatal and are reported without raw error
|
||||
values. Enabled auth requires a random 32+ character `AUTH_SECRET` and matching
|
||||
root `ORIGIN`/`AUTH_URL` with no trailing slash. Authenticated deployments use
|
||||
HTTPS; HTTP is supported only on `localhost` for local development. Missing or inconsistent auth settings
|
||||
report `auth_failed` followed by a fixed hint naming the failing environment
|
||||
variable and its requirement, without values or raw error details. Unknown auth
|
||||
initialization failures report only `auth_failed`. No mode falls back to open
|
||||
access after an auth failure.
|
||||
|
||||
Only a valid startup installs `watchConfig`, a non-persistent one-second path
|
||||
poll. Inode, modification-time or size changes request a serialized reload;
|
||||
editor atomic rename saves and deletion/recreation keep working. Boot requests
|
||||
one additional reload after watcher installation to catch saves during setup.
|
||||
Invalid later edits keep the last good snapshot and generic health readiness.
|
||||
|
||||
The request hook captures `directory.currentSnapshot()` once into
|
||||
`event.locals.snapshot`; nested loaders use that reference even if a reload
|
||||
publishes meanwhile. Boot resolves the public root once through the existing
|
||||
`resolvePublicDir` helper, using the same selected config path and environment as
|
||||
configuration loading. The application captures it as `event.locals.publicDir`;
|
||||
it never enters page data. Missing roots or files do not prevent startup. The page returns only the captured display model, a validated selected-link
|
||||
identity and a signed-in boolean after the access gate. Private configuration,
|
||||
assets, sessions and filesystem paths are not page data.
|
||||
`/healthz` returns only `{ "status": "ok" }` after initialization. Requests do not
|
||||
load YAML or regenerate bookmark logos. Shutdown stops the watcher and blocks pending
|
||||
publication. Polling/config preparation are not browser code.
|
||||
|
||||
Production lifecycle handling uses adapter-node's `sveltekit:shutdown` event.
|
||||
Development lifecycle handling separately owns replacement of previous instances,
|
||||
pending-startup cleanup, and the small serve-only Vite close bridge. Kit's pinned
|
||||
SSR loader does not run `import.meta.hot.dispose`, so that bridge remains
|
||||
application-specific. Development cleanup happens at the next initialization or
|
||||
server close, not immediately when source is invalidated.
|
||||
|
||||
### Configured image files
|
||||
|
||||
`/public/[...file]` checks exact membership in the captured
|
||||
`assets.referencedPaths` before any image I/O. The server-only `assets.ts` reader
|
||||
uses bounded 5 MiB reads, canonical containment, regular-file and stability checks,
|
||||
and the same opened descriptor's `/proc/self/fd` location. This requires Linux
|
||||
procfs even in native Node; unavailable descriptor lookup fails closed. Symlinks
|
||||
inside the root work, escapes do not. Extension/signature checks recognize
|
||||
PNG/JPEG/GIF/WebP and UTF-8 SVG without DTD/entity markup; this is neither a full
|
||||
codec/XML decoder nor an SVG sanitizer. SVG files are trusted operator content.
|
||||
|
||||
The route shares only response formatting with `/bookmarks/[id]/logo`; the
|
||||
snapshot renderer is unchanged. GET and HEAD return private/no-store, nosniff and
|
||||
byte lengths, with no HEAD body or range/conditional caching. Failures are generic
|
||||
404s. Config loads/preparation do not inspect images. References update only with
|
||||
accepted snapshots; image bytes update independently on subsequent requests.
|
||||
All mounted source URLs, including the selected site favicon's source, remain
|
||||
private when auth is enabled. HTML Accept does not turn image requests into
|
||||
sign-in redirects. The public group's `/favicon` GET/HEAD route reads only
|
||||
`locals.snapshot.configuration.site.favicon_path` through the existing reader and
|
||||
response helper. Selecting a bookmark image publishes its bytes through this alias
|
||||
as well, without making its raw URL public. A bundled selection (default or
|
||||
explicit) returns 404 before I/O; unavailable mounted bytes return generic 404,
|
||||
not a bundled fallback.
|
||||
|
||||
The public group's `/logo` GET/HEAD route likewise reads only the captured
|
||||
`site.logo_path`, with the same reader, headers, generic 404s and no query selectors.
|
||||
A bundled selection (default or explicit) returns 404 before I/O. Selecting an image also referenced by a bookmark
|
||||
publishes those bytes only through the alias, not its raw source URL.
|
||||
|
||||
The shared server layout maps resolved `/public/` site paths to constant
|
||||
`faviconPath` and `logoPath` aliases (`/favicon` and `/logo`), passing bundled
|
||||
`/favicon.svg` selections through unchanged. Defaults belong to shared config
|
||||
loading, not the layout. Its single head
|
||||
icon link has no fixed MIME type. No configured source URI, title, description,
|
||||
image bytes, session or private configuration enters root data; the existing public
|
||||
sign-in title remains page-owned. The server `themeStyleTag` helper selects the
|
||||
captured display's two validated primary colors and derives readable button
|
||||
foregrounds. A native Kit `transformPageChunk` inserts these as `--color-primary`
|
||||
and `--color-on-primary` on `:root` in the HTML head, not a layout data payload or
|
||||
wrapper. Shared `lib/styles/theme.css` owns static palettes, spacing, error colors,
|
||||
radii, `--outline-focus` and manual color schemes; page CSS owns geometry. Native `light-dark()` uses the OS scheme unless the root's
|
||||
`data-theme` selects light or dark (modern browser support required). Body palettes
|
||||
stay unchanged; the two primary colors and selected branding bytes are deliberately
|
||||
public before login. Theme inputs follow the existing full-document refresh
|
||||
contract after configuration reloads; query-only navigation retains the document theme.
|
||||
|
||||
With JavaScript, the protected header theme button cycles System → Dark → Light
|
||||
→ System. `directory.theme` in localStorage accepts only `system`, `dark` or
|
||||
`light`; invalid or unreadable values start in System. Blocked writes do not
|
||||
prevent toggling for the current document. The static nonce-bearing script in
|
||||
`app.html` restores the preference before head styles and hydration, including on
|
||||
public sign-in. The root records both `data-theme-preference` and the applied
|
||||
`data-theme`; OS changes update the latter only in System. There is no theme cookie
|
||||
or server-side preference. Without JavaScript, CSS follows the OS and the button
|
||||
stays disabled.
|
||||
|
||||
### Shared controls
|
||||
|
||||
Visible controls live in `apps/web/src/lib/components/ui`: `Button`, `ButtonLink`,
|
||||
`Input` and `Chip`, with appearance and interaction states owned by `controls.css`.
|
||||
Feature components own behavior, labels, icons and placement—not duplicate hover,
|
||||
focus, pressed, disabled or field styling. Root semantic theme colors remain the
|
||||
palette source; no separate UI theme is introduced.
|
||||
|
||||
Scoped styles belong with the component that renders the affected DOM. Use native
|
||||
layout wrappers for placement and animation, explicit presentation props for
|
||||
context-specific spacing, and forwarded headless-library attributes for semantics
|
||||
instead of reaching into child classes with `:global`. The class-prefixed shared
|
||||
UI stylesheet is intentionally shared; it does not style unrelated native elements.
|
||||
|
||||
`Button` forwards native button attributes/events and defaults to `type="button"`;
|
||||
submit controls must opt in. `ButtonLink` retains a real caller-provided href,
|
||||
target and rel. Both expose a native `element` binding, including forwarded Bits
|
||||
Close attachments. Primary actions keep their validated foreground/background
|
||||
colors through all states; quiet/icon and chip controls use restrained fills.
|
||||
|
||||
`Input` wraps its native input with optional leading/trailing snippets, forwarding
|
||||
id/name/ARIA/autocomplete/required/readonly/disabled and native events. Its `value`
|
||||
and `element` bindings refer to the actual string value and HTML input. The `class`
|
||||
prop belongs to the shell for layout hooks. Decorative leading content is an
|
||||
associated native label, so clicking it focuses the input without another handler.
|
||||
Only input focus highlights the shell; a trailing Clear button gets its own focus
|
||||
indicator, not a second field ring.
|
||||
`Chip` delegates interactive labels to Button and keeps informational labels as
|
||||
spans without scripting. Hidden form metadata remains a native hidden input.
|
||||
|
||||
### Progressive details
|
||||
|
||||
Every tile retains a real `/?link=<id>` URL with speculative preloading disabled;
|
||||
a current search is preserved as `q`. Dialog code is statically imported with the
|
||||
page, not fetched on first activation. JavaScript-enabled direct entries show the
|
||||
directory before the dialog's entrance, rather than a wide detail preview. Plain
|
||||
details and Back live in native `noscript` content and remain usable when JavaScript
|
||||
is disabled. The server-prepared global `site.open_links_in_new_tab` preference
|
||||
reaches both render paths: Open uses native `_blank` by default, or `_self` when
|
||||
false, with matching accessible text. Both retain `noopener noreferrer`; no click
|
||||
handler replaces browser navigation.
|
||||
|
||||
Kit 2 `pushState`/`replaceState` and typed `page.state` carry the selected ID,
|
||||
grid-entry marker and current query. Local opens, closes and Back/Forward use the
|
||||
loaded snapshot, without a data request. A real navigation or refresh accepts a
|
||||
fresh server snapshot. Grid close goes back once; direct close removes `link` while
|
||||
preserving `q`. Bits owns trapping, dismissal, scroll lock and animation presence.
|
||||
The mounted dialog retains its last link/group through the exit, then returns
|
||||
focus after native animation completion. Backdrop fades take 150ms, phone slides
|
||||
200ms, and desktop fades 150ms in both directions; reduced motion disables them.
|
||||
Ordinary dismissal returns to the activating tile or heading. Chip selection
|
||||
instead applies its query to the destination directory entry and focuses Search
|
||||
after exit, not to the detail entry that is being popped. The body portal inherits
|
||||
the configured accents naturally from the document root.
|
||||
|
||||
### Configuration update notice
|
||||
|
||||
The protected `/config/version` endpoint returns only the accepted snapshot ID
|
||||
with private/no-store caching. It follows the existing endpoint authentication
|
||||
boundary; rejected configuration details remain in operator diagnostics.
|
||||
|
||||
`ConfigUpdateNotice` lives in the root layout, observing the optional authorized
|
||||
page display through typed `App.PageData`. It captures the document's first display
|
||||
revision once, so later SPA data updates do not incorrectly mark old document CSS
|
||||
as current. It owns one request/timer, visibility/focus/online listeners, abort
|
||||
cleanup and bounded failure backoff. No checks run on public pages without display
|
||||
data, and no-JavaScript pages keep native refresh behavior. The full-width status
|
||||
bar sits above the header in document flow, stays sticky while scrolling, and
|
||||
remains below the modal overlay. It never takes focus. Its native link tracks the
|
||||
actual browser URL when Kit navigation state changes, including shallow query
|
||||
changes, and opts out of Kit navigation for a full document load. No invalidation
|
||||
or client theme bridge is needed.
|
||||
|
||||
### Local search
|
||||
|
||||
The header search shell is server-rendered but disabled until hydration. Its first
|
||||
focus or input dynamically imports `lib/search/engine.ts`, the only runtime Fuse
|
||||
import. Empty focus quietly warms the engine without moving the full directory;
|
||||
refocusing an unchanged query retains its results and images. Nonempty query
|
||||
replacements publish results without an intervening grouped directory. Tile-only
|
||||
visits do not load search unless the URL already contains a nonempty query. Normal
|
||||
text queries use Fuse.js 7.5.0's full-build token
|
||||
search with all query words required, independent of word order or field. The
|
||||
fixed threshold is 0.35 with location ignored; weights are title 4, prepared tags
|
||||
3, description 2 and remaining link-local public text 1. Unit fixtures cover
|
||||
ranking, typos, fragments, long queries and metadata boundaries.
|
||||
|
||||
A leading `tag:` matches the entire trimmed remainder against one whole prepared
|
||||
tag, using `toLowerCase()` on both sides. Spaces, punctuation and further colons
|
||||
are literal. A leading `access:` accepts only `login`, `internal` or `admin`,
|
||||
matching `requires_auth`, `internal_only` or `admin_only` strictly equal to `true`.
|
||||
Both prefixes and access values are case-insensitive. Empty known-prefix values
|
||||
and invalid access values return zero results, never fuzzy fallback. Other prefixes
|
||||
and non-leading prefix text remain normal Fuse queries. There are no quote escapes,
|
||||
operators, stacked filters or free-text suffixes: each query selects one mode.
|
||||
Exact modes filter the existing allowlisted documents in prepared display order
|
||||
across groups, preserving original link references without mutating field data.
|
||||
|
||||
The index includes only explicit bookmark text: title, prepared tags, description,
|
||||
ID, URL, group label and enabled advisory labels. Image references, private config
|
||||
and common site copy are not search content. One successful index is retained for
|
||||
the loaded snapshot ID; real navigation accepting a different snapshot rebuilds
|
||||
it. Search never requests new configuration. The visible query is represented by
|
||||
`q` in the URL: typing replaces the current entry, Clear removes it, and shared or
|
||||
reloaded queries activate search after hydration without taking focus. Typed
|
||||
`page.state.directoryQuery` carries the same query through Kit shallow history,
|
||||
whose APIs do not update `page.url`; real navigation falls back to the URL value.
|
||||
No local storage or server search is involved.
|
||||
|
||||
Nonempty queries show flat tiles with group captions, ranked for normal text and
|
||||
in prepared display order for exact prefixes. Selecting a detail chip closes details,
|
||||
replaces the current query with its `tag:` or `access:` query and focuses Search.
|
||||
Without JavaScript, chips remain informational. Empty/whitespace queries restore
|
||||
the original groups and order. Clear returns focus to the input;
|
||||
result counts, loading, no-results and failure messages are announced. Positive
|
||||
counts and keyboard help are screen-reader-only; loading, empty and failure
|
||||
messages remain visible without reserving a success-feedback gap. A failed
|
||||
import leaves the full grid usable and suggests reloading, without an automatic
|
||||
retry or reload loop. Without JavaScript the full HTML and detail URLs still work.
|
||||
Search remains a labelled native search field, not a selection popup. Enter opens
|
||||
details when exactly one result is ready, focuses the first of several, and does
|
||||
nothing while loading or with no matches. Down focuses the first ready result.
|
||||
Filtered results form a normal list of real detail links. Tab and Shift-Tab visit
|
||||
every result in document order. Optional arrows follow the visual CSS rows without
|
||||
wrapping at boundaries; Home/End move to the first/last result in the current row.
|
||||
CSS handles responsive wrapping without recreating links or their images. Escape
|
||||
from a result focuses Search and preserves the query; Escape in Search or Clear
|
||||
restores grouped browsing. IME composition and modified keys do not trigger these
|
||||
commands. Detail activation is synchronous and uses the already-loaded dialog.
|
||||
Search is discovery, not access control: details always resolve against the full
|
||||
loaded directory. Shallow dialog history preserves query/results and returns
|
||||
focus to the activating tile, falling back to the directory heading when that result
|
||||
is no longer present. Chip selection instead returns focus to Search with the new
|
||||
query. Access flags remain advisory; filtering does not enforce service access.
|
||||
|
||||
### Navigation error recovery
|
||||
|
||||
Kit navigation errors use a centered, themed page with the HTTP status, a fixed
|
||||
heading and a native Back to home link. That link requests a full page load at `/`,
|
||||
so recovery does not depend on a failed client route module.
|
||||
The page does not render raw error messages, stacks or requested URLs, and makes
|
||||
no automatic retry. Authentication redirects and plain API denials remain owned
|
||||
by the access gate; this page does not bypass or replace them.
|
||||
|
||||
### Authentication boundary
|
||||
|
||||
The running application owns its directory and one boot-created Auth.js instance
|
||||
when auth is configured. Providers and seven-day JWT session policy do not change
|
||||
on display reloads. The request hook captures the application and one snapshot,
|
||||
then runs Auth.js and the whole-directory access checks. Only a signed-in boolean is
|
||||
added to the directory page data; sessions, provider configuration and Auth.js handles
|
||||
remain server-side.
|
||||
|
||||
Route folders declare access ownership; SvelteKit groups do not change URLs:
|
||||
|
||||
```text
|
||||
routes/
|
||||
+layout.server.ts
|
||||
+layout.svelte
|
||||
(public)/
|
||||
favicon/
|
||||
logo/
|
||||
signin/
|
||||
healthz/
|
||||
(protected)/
|
||||
+page.server.ts
|
||||
+page.svelte
|
||||
(endpoints)/
|
||||
bookmarks/[id]/logo/
|
||||
public/[...file]/
|
||||
```
|
||||
|
||||
With authentication enabled, the hook bypasses the session gate only for matched
|
||||
`(public)` route IDs. All other application routes, including unmatched requests,
|
||||
require a session by default. Anonymous HTML GET page navigation redirects to
|
||||
sign-in; data, HEAD and non-navigation requests return 401. The nested `(endpoints)`
|
||||
group always returns 401 to anonymous requests, even with HTML Accept, and is also
|
||||
where future private APIs belong. Route handlers and Kit reject unsupported verbs;
|
||||
public ownership does not permit arbitrary writes. Configured image references do
|
||||
not create access exceptions. Bundled static files are served by adapter-node or
|
||||
Vite before application hooks, not by a public URL-prefix rule in the gate.
|
||||
No-auth mode, origin/CSRF checks and the native Auth.js sequence are unchanged.
|
||||
|
||||
Sign-in uses native SvelteKit form actions, as in When, with a server-selected
|
||||
provider and an allowlist of submitted fields. Return URLs are reduced to safe
|
||||
local paths. Sign-out is the root page's named POST action. Login forms work
|
||||
without JavaScript. Desktop fine-pointer devices at least 48rem wide focus the
|
||||
username or SSO button on mount; phones and no-JavaScript pages do not autofocus.
|
||||
Sign-in uses a centered desktop card and a cardless phone layout with 48px controls,
|
||||
fixed generic error banners and reduced-motion-aware states. Auth.js owns session
|
||||
encryption/cookies and OIDC callbacks;
|
||||
Directory owns default-deny routing, safe diagnostics and fixed-origin policy.
|
||||
Auth.js is bundled server-side; its build-time dependency classification keeps
|
||||
framework/compiler peers out of the pruned container runtime.
|
||||
|
||||
SvelteKit's CSRF protection stays enabled, with an additional exact-origin check
|
||||
on state-changing requests independent of content type. Auth.js uses its standard
|
||||
`AUTH_URL` setting for callback/action URLs. Session handling uses the native
|
||||
SvelteKit event and cookie context; only submitted form fields are allowlisted. Native CSP protects scripts and framing;
|
||||
it does not restrict form redirects to self, because Chromium would block the
|
||||
OIDC redirect to the identity provider. Responses returned through the outer
|
||||
application hook receive `nosniff` and `strict-origin-when-cross-origin` headers in
|
||||
both authenticated and open-access modes, including auth redirects and denials.
|
||||
Kit-generated responses outside that hook, such as slash normalization, early CSRF
|
||||
rejections and conditional 304s, are not covered by this policy. Referrer policy
|
||||
preserves form Origin headers without leaking directory paths to another origin.
|
||||
This baseline does not change authentication's private/no-store caching or
|
||||
adapter-served static asset caching. Cookie security uses the native Auth.js and
|
||||
SvelteKit defaults for HTTPS and HTTP localhost.
|
||||
|
||||
Server diagnostics use **tslog 5.2.0**: JSON in production and pretty output in
|
||||
development, with call-site stack capture disabled. Reporting passes only fixed
|
||||
messages, issue codes and safe field paths to the logger, never raw errors or
|
||||
configuration values. Library formatting is not a substitute for that boundary.
|
||||
|
||||
### Browser checks
|
||||
|
||||
Browser tests launch `node build/index.js` from the web workspace on
|
||||
`127.0.0.1:4183`. They refuse to reuse an existing server and stop their server
|
||||
afterwards. Build before invoking E2E independently. Playwright explicitly sets
|
||||
`CONFIG_PATH` to the checked-in empty-directory fixture under `e2e/fixtures`, not
|
||||
the process's operator path. Tests use no personal configuration, credentials,
|
||||
or development server. Unit discovery is separate from browser specs.
|
||||
The existing web `check` command also type-checks `playwright.config.ts` and all
|
||||
`e2e/**/*.ts` files, including support helpers. Its tsconfig restates Kit's generated
|
||||
includes before adding browser tests; keep that block aligned when upgrading Kit.
|
||||
Application tests use only their own fixtures and disposable data, never the
|
||||
manual `demo/` configuration or images. The demo is not an automated test, build,
|
||||
lint, formatting or package input and does not trigger CI.
|
||||
|
||||
The co-located process tests launch the real adapter-node build with disposable
|
||||
YAML and OS-assigned ports. They check fatal invalid configuration or auth
|
||||
prerequisites, atomic replacement, invalid/deleted file recovery, safe logs/health and graceful
|
||||
shutdown. These tests require the production build, just like the CLI artifact
|
||||
tests; they are not browser tests. Coverage measures TypeScript modules, not Svelte markup;
|
||||
there is no arbitrary percentage gate or claim of full UI coverage. The config
|
||||
smoke test imports built output, so its pass does not imply source instrumentation
|
||||
coverage of `src/index.ts`.
|
||||
|
||||
Authentication browser/HTTP tests use separate disposable production servers and
|
||||
a test-only local OIDC issuer built from Node HTTP/crypto. They exercise real
|
||||
Auth.js flows, discovery, PKCE/state/nonce, invalid claims/outages, cookie expiry/rotation,
|
||||
private data routes and cross-origin/redirect attacks. Configured-image tests use
|
||||
disposable mounts for real production HTTP authentication, exact references,
|
||||
symlinks, MIME, HEAD/cache headers, reloads and byte recovery, plus the existing
|
||||
SSR logo consumer with JavaScript disabled. Reader unit tests cover bounded I/O,
|
||||
ordinary races and descriptor cleanup. These are not Docker/procfs deployment
|
||||
checks. No live IdP or operator
|
||||
config is used; real HTTPS/proxy and provider registration remain manual checks.
|
||||
|
||||
Root commands cover all three workspaces, ordered by their declared package
|
||||
dependencies. Each workspace's lint/format commands explicitly share the root
|
||||
ignore file, so formatting after a build/test run must not rewrite generated
|
||||
output. Build output, coverage, operator configuration, local plans, manual demo
|
||||
data, and prototypes remain outside formatting; ESLint also ignores `demo/`.
|
||||
|
||||
## Manual app releases
|
||||
|
||||
Directory follows When's thin Changesets CLI pattern, with no release bot,
|
||||
prerelease flow or automatic commit, tag push or deployment. The three root
|
||||
commands are `pnpm changeset`, `pnpm version:apps` and `pnpm tag:apps`; root
|
||||
quality commands still cover all workspaces, not just web.
|
||||
|
||||
The fixed group contains only `@directory/web` and `@directory/cli`. Both are
|
||||
private, but `privatePackages.version` and `privatePackages.tag` are enabled so
|
||||
Changesets can version and tag them together. `commit: false` keeps commits
|
||||
manual; `baseBranch: main` is the comparison base. `@directory/config` is internal
|
||||
and ignored for independent versioning, **not ignored for release impact**:
|
||||
record behavior/schema changes in an explicit changeset for each affected
|
||||
consuming app. Its checks remain required. Tooling/docs-only setup needs no
|
||||
retroactive app changeset or version bump.
|
||||
|
||||
1. With each feature or fix, run `pnpm changeset`. Select the affected app(s),
|
||||
choose major/minor/patch by consumer impact and write a useful release note.
|
||||
Review the generated `.changeset/*.md` with the feature. The fixed group
|
||||
synchronizes app versions at release time, even for a web-only change.
|
||||
2. When preparing a release on reviewed main, inspect pending changesets with
|
||||
`pnpm changeset status`, then manually run `pnpm version:apps`. This consumes
|
||||
pending entries, updates app manifests and generates app changelogs. Review
|
||||
the complete diff, including any lockfile changes, and verify the intended
|
||||
versions and notes. Do not hand-bump versions or run this in CI.
|
||||
3. Run the frozen-install and all-workspace quality gates (`pnpm verify`), then
|
||||
obtain owner review and explicit authorization before committing the release
|
||||
manifests, changelogs, consumed changesets and any lockfile changes. Do not
|
||||
mix unrelated working changes into the release commit.
|
||||
4. Only after the release commit is approved, and tagging is explicitly
|
||||
authorized, run `pnpm tag:apps`. It creates local qualified tags such as
|
||||
`@directory/web@1.2.3` and `@directory/cli@1.2.3`; it does not push them.
|
||||
Inspect tags and their target commit. Push the approved release commit and
|
||||
only the intended release tags when separately authorized; do not blindly
|
||||
push every local tag.
|
||||
5. Inspect both providers' runs and registry outputs. Only the qualified web tag
|
||||
publishes the web image; the accompanying CLI tag must not publish a second
|
||||
image. There is no npm publication or separately published CLI image.
|
||||
|
||||
The publisher accepts only stable `@directory/web@<major>.<minor>.<patch>` tags
|
||||
with no leading zeroes, matching `apps/web/package.json` exactly. Partial,
|
||||
prerelease, build-metadata, malformed and mismatched tags are rejected before
|
||||
registry login/publication. After web checks/E2E and schema/config/CLI gates,
|
||||
`@directory/web@1.2.3` publishes `1.2.3`, `1.2` and `1` image aliases to both
|
||||
`git.eleith.com/eleith.private/directory` and `ghcr.io/eleith/directory`. Tags
|
||||
never move `latest`; only successful main pushes do. Minor/major aliases move
|
||||
when a release is published; select a full version or digest for deployment
|
||||
pinning. See [published images](deployment.md#published-production-images).
|
||||
|
||||
Rehearse authoring, versioning and tagging only in an independent disposable Git
|
||||
repository, never the working repository or a worktree that shares its refs.
|
||||
Before a real release, check private-app lockstep, ignored-config app impact,
|
||||
consumed notes/changelogs and qualified tags there. Hosted publication and
|
||||
registry authorization still require actual authorized provider runs.
|
||||
|
||||
## Continuous integration
|
||||
|
||||
Each provider has separate `config.yml`, `cli.yml`, and `web.yml` pipelines,
|
||||
matching `when`: [.github/workflows](../.github/workflows/) and
|
||||
[.woodpecker](../.woodpecker/). `pnpm verify` remains the local all-workspace
|
||||
aggregate, not a pipeline. Every selected pipeline uses a frozen full-workspace
|
||||
install, then package-scoped build, type checks, lint, and coverage tests. Config
|
||||
exports and CLI executables are built before their artifact tests. Both config
|
||||
pipelines run `pnpm schema:check` after frozen installation and before building;
|
||||
regeneration is never an automatic part of build. Web alone has
|
||||
production browser tests, with an explicit production build before Playwright.
|
||||
Each pipeline also runs root `pnpm lint` so global formatting/ESLint remains
|
||||
covered; this intentionally overlaps workspace lint.
|
||||
|
||||
Path filters include dependencies and shared tooling, not just app directories:
|
||||
|
||||
- `packages/config/**` and the shared `apps/web/config/directory.example.yml`
|
||||
starter run config, CLI, and web checks. The manual-only `demo/` does not trigger
|
||||
pipelines or supply automated test fixtures.
|
||||
- `apps/cli/**` or `apps/web/**` selects that application's pipeline.
|
||||
- Root files (`'*'`, including dotfiles), `.changeset/**`, `docs/**`, and both
|
||||
workflow directories run all three. This covers the lockfile, catalog,
|
||||
package manager/Node pins, base TypeScript, ESLint/Prettier settings, docs, and
|
||||
workflow changes.
|
||||
|
||||
Both providers check main pushes and pull requests. The web pipeline also
|
||||
selects qualified web-tag candidates and runs all workspace gates before image
|
||||
publication, even when the other package pipelines do not select tag events.
|
||||
Keep these filters and the package gates current when adding shared tools or
|
||||
commands.
|
||||
|
||||
- GitHub Actions uses `ubuntu-24.04`, Node 26.5.0, pnpm from the `packageManager`
|
||||
pin, and a pnpm store cache. Job names are distinct: `Config checks`, `CLI
|
||||
checks`, `Web checks`, and `Web E2E`. Only web has a dependent E2E job, using
|
||||
`ghcr.io/eleith/containers-playwright:1.61.1-node26` pinned to its verified
|
||||
manifest digest. That separate checkout gets another frozen install and builds
|
||||
only web and its shared dependencies before browser tests.
|
||||
- Each Woodpecker pipeline shares `/workspace/src` across ordered preparation,
|
||||
package build/check/lint/test steps, and web E2E where applicable. Plain Node
|
||||
steps use `node:26.5.0-bookworm`, pinned Corepack 0.36.0, and pnpm 11.9.0.
|
||||
Corepack's shared cache is under the ignored `.pnpm-store/corepack`; the pnpm
|
||||
store is local too. Later steps reuse dependencies and output. Web E2E uses
|
||||
`git.eleith.com/eleith.private/playwright:1.61.1-node26` without overriding its
|
||||
baked Corepack cache settings.
|
||||
|
||||
The browser images contain Playwright 1.61.1's browsers and Ubuntu 24.04 system
|
||||
libraries; CI does not download browsers or run APT for each application check.
|
||||
Keep both image references (and the GitHub digest) paired with the catalog's
|
||||
Playwright version. A `node26` image tag alone does not pin a Node patch: browser
|
||||
steps explicitly require Node 26.5.0 and pnpm 11.9.0. The public GHCR image's
|
||||
contents were inspected; inspect runner logs after image changes to confirm the
|
||||
separately built Gitea image too. A mismatched image fails rather than silently
|
||||
using another toolchain. Local development still needs its own matching browser
|
||||
installation as described above.
|
||||
|
||||
These workflows do not inject operator YAML/application credentials, write package
|
||||
versions or deploy. Release versioning and tag pushes remain manual. PR jobs
|
||||
retain `contents: read`; the public GHCR browser image requires no registry
|
||||
login. The Woodpecker runner
|
||||
needs pull access to the existing Gitea CI image, as for `when`. Configure read
|
||||
access at the CI platform, not by exposing publishing credentials to application
|
||||
commands or pull requests.
|
||||
|
||||
### Main and release image publication
|
||||
|
||||
Main pushes and validated stable web tags add image publication only after web
|
||||
checks and production E2E.
|
||||
Both providers also run explicit schema/config/CLI gates before publishing, so
|
||||
parallel package pipelines cannot allow a failing dependency check to be bypassed.
|
||||
The Docker build copies pruned production dependencies and compiled output into
|
||||
the non-root image. It needs no operator YAML or auth secret at build time. See
|
||||
[Docker and Compose](deployment.md) for local use and the owner's container,
|
||||
bind-mount and image-size checks.
|
||||
|
||||
- Woodpecker uses `woodpeckerci/plugin-docker-buildx:6.0.3` to publish
|
||||
`git.eleith.com/eleith.private/directory`, matching When's plugin, registry and
|
||||
`gitea_docker_registry` secret. Main publishes `latest`; stable web tags use
|
||||
the validated `.tags` file for full/minor/major aliases. Allow this plugin in
|
||||
the runner's privileged-plugin configuration. Restrict the secret
|
||||
**server-side** to `push` and `tag` events and this exact plugin; no PR access,
|
||||
including higher-priority secret overrides. Woodpecker has no native branch
|
||||
filter for secrets: workflow branch/tag conditions are not a credential
|
||||
boundary. Trust everyone who can push workflow changes, or enforce stronger
|
||||
policy through your secret backend.
|
||||
Registry credentials belong only in plugin settings, never application commands
|
||||
or build arguments.
|
||||
- GitHub's `Publish web image` job depends on checks/E2E and runs only for main
|
||||
pushes or qualified stable web tags. Only that job receives `packages: write`;
|
||||
it authenticates to GHCR with `GITHUB_TOKEN` and publishes
|
||||
`ghcr.io/eleith/directory`. `docker/metadata-action@v6` disables automatic
|
||||
`latest`, selecting it explicitly for main and full/minor/major aliases for
|
||||
validated web tags. Confirm the repository/package link and write
|
||||
authorization at the provider. Normal PR
|
||||
jobs do not run registry login or receive publication credentials.
|
||||
|
||||
Both publish the `prod` target for `linux/amd64`. CLI tags do not publish images;
|
||||
release tags never update main's `latest`. Registry availability, token scope
|
||||
and runner capability must be confirmed on the real
|
||||
providers; static validation is not a successful image build or publication.
|
||||
|
||||
### Woodpecker email notifications
|
||||
|
||||
Every package pipeline has one `notify` step using
|
||||
`deblan/woodpecker-email:3.3.0`, with `status: [failure, success]`. It runs only on
|
||||
main pushes, never PRs. The plugin infers the result from the CI status, so no
|
||||
explicit `level` setting or duplicate steps are needed. Configure or confirm these existing secret names
|
||||
in the effective repository/organization/global secret scope:
|
||||
|
||||
- `woodpecker_email_dsn`
|
||||
- `woodpecker_email_from_address`
|
||||
- `woodpecker_email_from_name`
|
||||
- `woodpecker_email_recipient`
|
||||
|
||||
Restrict all four secrets server-side to the `push` event and the email plugin
|
||||
image/tag (`deblan/woodpecker-email:3.3.0`). Ensure the allowlist includes this
|
||||
version; retain tags still required by other repositories sharing these secrets.
|
||||
Do not enable PR access: YAML conditions alone cannot protect secrets
|
||||
from a PR that changes the workflow. Bind secrets only in notification settings,
|
||||
not application commands. Missing SMTP settings/delivery remain operator setup,
|
||||
not a passing notification test. Common-path changes can produce three package
|
||||
emails rather than one aggregate message.
|
||||
|
||||
Enable the repository in Woodpecker with a Linux amd64 container-backed runner
|
||||
that can pull the CI images, and place/enable the workflow in the intended GitHub
|
||||
repository or mirror. A Gitea pull request does not automatically create a GitHub
|
||||
pull request or trigger that provider's PR event. Check actual runs on each
|
||||
provider separately; YAML checks, image inspection, and local `pnpm verify` are
|
||||
not successful hosted CI runs. Review branch-protection/required-check settings
|
||||
before enabling path filters: GitHub can leave a required check pending when its
|
||||
entire workflow is skipped. Do not blindly require every selectively triggered
|
||||
package check. Distinct job names avoid ambiguous statuses but do not solve that
|
||||
provider policy issue.
|
||||
|
||||
Schema generation, YAML loading, CLI validation and Compose commands are available.
|
||||
Release commands are manual as described above. Docker execution and final
|
||||
image checks, axe and PageSpeed remain owner-run manual checks, not
|
||||
agent-verified gates.
|
||||
|
||||
## Manual checks
|
||||
|
||||
For presentation review, use disposable fixture data with at least nine bookmarks,
|
||||
spaced and long unbroken titles, named groups and unassigned links. Check 360, 768
|
||||
and 1280px widths in both OS themes, with and without JavaScript: centered 6rem
|
||||
tiles, wrapped titles, no horizontal overflow, and distinct group/detail/footer
|
||||
spacing. Tab through tiles, Back and Open to inspect the configured accent
|
||||
and visible focus. Capture phone/desktop light/dark screenshots outside the repo.
|
||||
Also review flat and empty directories; the empty state should explain how to add links.
|
||||
Use Tab, Enter and Space on the theme button, including at 360px beside Sign out.
|
||||
Check the next-action label and visible focus, cycle all three modes, change the
|
||||
OS scheme in System and manual modes, then refresh and navigate details/Back.
|
||||
Throttle hydration to check restoration before paint; block localStorage to check
|
||||
usable in-memory toggling. Sign out with a saved manual theme and confirm public
|
||||
sign-in keeps that scheme and approved public branding, with no private site data
|
||||
or CSP script errors. Disable JavaScript to check OS colors and native navigation.
|
||||
Tab first to Search, then Theme and Sign out; check search focus and Clear at
|
||||
360/1280px in both schemes. Try typos, reversed multiword queries, tags and deep
|
||||
description text; clear or enter whitespace to restore configured grouping.
|
||||
Inspect the network before focus and after tile-only activation: no search/Fuse
|
||||
chunk should load. Throttle search loading, change/clear the query, and block the
|
||||
chunk to check announcements and the usable full-grid fallback. Open a result,
|
||||
Close/Back/Forward, and confirm the query and return focus survive. A native
|
||||
navigation or refresh should accept replacement metadata, while local search and
|
||||
dialog actions keep their loaded snapshot.
|
||||
|
||||
1. Follow the clean setup in an isolated export of the staged commit. Check root
|
||||
verification, CLI help and validation of both the production starter and demo without
|
||||
personal config. Use a disposable file with a missing environment reference
|
||||
or malformed YAML and check the failure output stays redacted. In a YAML-aware
|
||||
editor, check template syntax and required-field diagnostics.
|
||||
2. After changes are authorized and pushed, inspect a Woodpecker PR
|
||||
run and a GitHub PR/main run on the configured providers. Confirm package
|
||||
routing: config/root/tooling/docs/workflow changes fan out; application-only
|
||||
changes select their own pipeline. Check pinned tools, frozen install, package
|
||||
gates, and web's preinstalled browsers/production E2E. Review required-check
|
||||
policy against skipped workflows. An absent runner or GitHub mirror remains
|
||||
an outstanding operator setup task, not a passing check.
|
||||
3. Review events and permissions: PR checks must not receive SMTP or publishing/
|
||||
deployment secrets and must not send email or push images. Confirm SMTP event/
|
||||
plugin restrictions at the server, including any higher-priority secret
|
||||
overrides. On an authorized main run, check package notifications actually
|
||||
arrive. Verify failure delivery in a controlled runner test, not by breaking
|
||||
main. Do not inject operator YAML or credentials into application checks.
|
||||
4. Make a disposable copy to try startup and file changes without editing the
|
||||
checked-in demo or operator configuration:
|
||||
|
||||
```sh
|
||||
preview=$(mktemp -d)
|
||||
cp demo/config/directory.yaml "$preview/directory.yaml"
|
||||
CONFIG_PATH="$preview/directory.yaml" \
|
||||
AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
ORIGIN=http://localhost:5173 AUTH_URL=http://localhost:5173 pnpm dev
|
||||
```
|
||||
|
||||
Run from the repository root; open `http://localhost:5173` and use
|
||||
**demo / directory-demo**. This generates a unique demo-only secret per run.
|
||||
Check desktop/narrow widths, OS light/dark and JavaScript disabled. Confirm
|
||||
all 32 generated default logos load on cards and details across five uneven
|
||||
groups; site branding uses the bundled default and service URLs are placeholders.
|
||||
No image mounts or asset copies are needed for this manual demo. Edit the
|
||||
copied site's title/description and refresh after approximately one second.
|
||||
Try an atomic rename save, invalid YAML, deletion and restoration: the last
|
||||
good copy should remain until a valid file returns. `/healthz` exposes only
|
||||
readiness, not configuration names or paths. Configured authentication must
|
||||
require sign-in before site copy/data are returned; missing secret/origin must
|
||||
fail startup. Try a wrong password, sign-out and a direct `__data.json` request.
|
||||
Follow a bookmark to its `?link=<id>` details and close the dialog. Without
|
||||
JavaScript, return with Back to directory. Check Tab/Shift-Tab trapping, Escape,
|
||||
outside dismissal and return focus, then browser Back/Forward and a direct URL.
|
||||
Throttle or block page hydration scripts to review usable server-rendered details.
|
||||
Check that a generated logo renders without JavaScript and remains protected before sign-in,
|
||||
including HEAD and `/bookmarks/<id>/logo` opened as HTML. For a configured logo,
|
||||
use a disposable public directory and set `DIRECTORY_PUBLIC_DIR` to its absolute
|
||||
path before starting. Check mounted logos on cards/details without JavaScript,
|
||||
generic 404s for unreferenced or missing files, then atomic file replacement
|
||||
and recovery. To exercise browser fallback manually, block a configured bookmark
|
||||
image in browser devtools or choose a missing image path in the disposable YAML.
|
||||
With JavaScript, cards and details should switch once to their prepared logo;
|
||||
blocking that endpoint too must stop without a retry loop. Disable JavaScript
|
||||
to check broken images leave the text and links usable. Unblock or repair the
|
||||
primary and refresh to try the unchanged URL again. In the disposable config,
|
||||
set `site.logo_path` to check a 48px header logo beside wrapped title/description
|
||||
at 360px, with Sign out still visible. Omit it to check the bundled default;
|
||||
break it to check the one-time JavaScript fallback and no-JavaScript limitation.
|
||||
Before sign-in, check selected logo bytes through `/logo`, independent of the
|
||||
tab's `/favicon`; configured source URIs must not appear in public HTML/data.
|
||||
Check configured primary/button foreground contrast, the centered desktop card,
|
||||
cardless phone, generic error banner and native POST. Desktop fine-pointer
|
||||
autofocus must not open a phone keyboard; no-JavaScript first Tab is Username.
|
||||
Leave the curated demo files untouched.
|
||||
Authenticated mounted GET/HEAD work; unauthenticated requests
|
||||
return 401 even with HTML Accept, including the selected favicon's raw source.
|
||||
Before sign-in, check the tab icon and `/favicon` GET/HEAD: only the selected
|
||||
bytes are public, with private/no-store, nosniff and bodyless HEAD. Change the
|
||||
selected source and check the same alias returns new bytes after polling;
|
||||
invalid YAML and auth-changing edits must keep the last good selection. Omit
|
||||
the field to check the head uses `/favicon.svg` and the alias returns 404.
|
||||
Missing or invalid selected files must return 404, not the bundled icon. Verify
|
||||
Linux procfs availability.
|
||||
See the [owner Docker checklist](deployment.md#owner-docker-review-checklist)
|
||||
for the separate real-image/mount/procfs review. Stop the server when finished.
|
||||
For production output, after `pnpm build`, run:
|
||||
|
||||
```sh
|
||||
CONFIG_PATH="$PWD/demo/config/directory.yaml" \
|
||||
AUTH_SECRET="$(openssl rand -base64 32)" \
|
||||
ORIGIN=http://localhost:3000 AUTH_URL=http://localhost:3000 \
|
||||
HOST=127.0.0.1 PORT=3000 node apps/web/build/index.js
|
||||
```
|
||||
|
||||
Run from the repository root. Open `http://localhost:3000`, sign in with
|
||||
**demo / directory-demo**, then stop the server when finished. This production
|
||||
preview also generates a demo-only secret per run. The
|
||||
directory uses prepared bookmark logos for omitted logo paths and serves exact
|
||||
configured image references. Review local search with the checks above. Run
|
||||
manual axe/PageSpeed on this visible baseline when available; this slice is not
|
||||
final UI acceptance.
|
||||
|
||||
Complete agent review before staged owner review. Stage only one coherent commit
|
||||
at a time when requested, and wait for manual review and explicit permission
|
||||
before committing or pushing. A successful local check is not a hosted CI run.
|
||||
keep changes small, well tested, well linted and well.
|
||||
@@ -0,0 +1,253 @@
|
||||
# Replace the placeholder addresses with your own.
|
||||
# Sign-in is disabled. Uncomment one auth example below to enable it.
|
||||
version: 1
|
||||
site:
|
||||
title: Home Server
|
||||
description: Useful things running at home.
|
||||
footer: Something not working? Ask your household maintainer.
|
||||
|
||||
appearance:
|
||||
primary_light_color: '#0f766e'
|
||||
primary_dark_color: '#5eead4'
|
||||
|
||||
# Shared sign-in. Use your directory's HTTPS address, with no trailing slash.
|
||||
# HTTP localhost is allowed for local testing. Restart after changing auth.
|
||||
# auth:
|
||||
# origin: https://directory.example.com
|
||||
# credentials:
|
||||
# username: admin
|
||||
# password: replace-with-your-own-password
|
||||
|
||||
# Or SSO. Register this callback with your provider:
|
||||
# https://directory.example.com/auth/callback/oidc
|
||||
# auth:
|
||||
# origin: https://directory.example.com
|
||||
# oidc:
|
||||
# issuer: https://identity.example.com
|
||||
# client_id: directory
|
||||
# client_secret: replace-with-your-client-secret
|
||||
|
||||
# Restarts sign everyone out unless you set auth.secret. To keep sessions,
|
||||
# generate a value once with: openssl rand -base64 32
|
||||
# Add it inside your chosen auth block as: secret: 'your-generated-value'
|
||||
# Passwords and secrets can also use '${VAR}' from an optional .env file.
|
||||
# Keep files containing secrets private.
|
||||
|
||||
# Reserved example.com destinations are placeholders, not live integrations.
|
||||
# All bookmarks use generated logos; site branding uses the bundled default.
|
||||
links:
|
||||
photos:
|
||||
title: Photos
|
||||
url: https://photos.example.com
|
||||
description: |
|
||||
Family photos and videos, all in one place.
|
||||
Install your photo service's mobile app for automatic uploads.
|
||||
tags: [photos, videos, memories]
|
||||
group: Photos & files
|
||||
requires_auth: true
|
||||
files:
|
||||
title: Files
|
||||
url: https://files.example.com
|
||||
description: Shared documents, school projects and household paperwork.
|
||||
tags: [documents, sharing, storage]
|
||||
group: Photos & files
|
||||
backups:
|
||||
title: Backups
|
||||
url: https://backups.example.com
|
||||
description: |
|
||||
Check the latest backup status before you need a restore.
|
||||
Ask your household maintainer for help recovering a file.
|
||||
tags: [backup, recovery, storage]
|
||||
group: Photos & files
|
||||
scans:
|
||||
title: Paperwork inbox
|
||||
url: https://scans.example.com
|
||||
description: Sort recent scans into searchable bills, receipts and warranties.
|
||||
tags: [scans, documents, receipts]
|
||||
group: Photos & files
|
||||
archive:
|
||||
title: Family history archive
|
||||
url: https://archive.example.com
|
||||
description: Old letters, digitized albums and notes about the family tree.
|
||||
tags: [history, memories, documents]
|
||||
group: Photos & files
|
||||
albums:
|
||||
title: Shared albums
|
||||
url: https://albums.example.com
|
||||
description: Choose a photo collection to share with visiting relatives.
|
||||
tags: [photos, albums, sharing]
|
||||
group: Photos & files
|
||||
transfers:
|
||||
title: File drop
|
||||
url: https://transfers.example.com
|
||||
description: Temporary space for large files moving between household devices.
|
||||
tags: [transfer, sharing, storage]
|
||||
group: Photos & files
|
||||
internal_only: true
|
||||
notebooks:
|
||||
title: Household notebooks
|
||||
url: https://notebooks.example.com
|
||||
description: Shared notes for school, travel plans and everyday reference.
|
||||
tags: [notes, writing, planning]
|
||||
group: Photos & files
|
||||
sync:
|
||||
title: Device sync status
|
||||
url: https://sync.example.com
|
||||
description: Check whether laptop folders have reached the home server.
|
||||
tags: [sync, devices, storage]
|
||||
group: Photos & files
|
||||
internal_only: true
|
||||
keepsakes:
|
||||
title: DigitalKeepsakesAndSchoolProjectCollections
|
||||
url: https://keepsakes.example.com
|
||||
description: Finished school projects and small creations worth keeping.
|
||||
tags: [school, archive, memories]
|
||||
group: Photos & files
|
||||
music:
|
||||
title: Music
|
||||
url: https://music.example.com
|
||||
description: Albums, playlists and a soundtrack for dinner.
|
||||
tags: [music, audio, playlists]
|
||||
group: Entertainment
|
||||
movies:
|
||||
title: Movies
|
||||
url: https://movies.example.com
|
||||
description: Browse the family movie and television library.
|
||||
tags: [movies, television, streaming]
|
||||
group: Entertainment
|
||||
books:
|
||||
title: Books
|
||||
url: https://books.example.com
|
||||
description: Find your next ebook or audiobook for the commute.
|
||||
tags: [books, reading, audiobooks]
|
||||
group: Entertainment
|
||||
radio:
|
||||
title: Radio
|
||||
url: https://radio.example.com
|
||||
description: Saved station links and recordings from the community radio club.
|
||||
tags: [radio, audio, recordings]
|
||||
group: Entertainment
|
||||
podcasts:
|
||||
title: Podcast queue
|
||||
url: https://podcasts.example.com
|
||||
description: Episodes downloaded for the next long drive.
|
||||
tags: [podcasts, audio, travel]
|
||||
group: Entertainment
|
||||
reading:
|
||||
title: Weekend reading list
|
||||
url: https://reading.example.com
|
||||
description: Saved articles and reading suggestions from around the household.
|
||||
tags: [reading, articles, bookmarks]
|
||||
group: Entertainment
|
||||
requests:
|
||||
title: Library requests
|
||||
url: https://requests.example.com
|
||||
description: Suggest something for the household media collection.
|
||||
tags: [library, requests, media]
|
||||
group: Entertainment
|
||||
requires_auth: true
|
||||
home-automation:
|
||||
title: Home automation
|
||||
url: https://home.example.com
|
||||
description: |
|
||||
Lights, room temperatures and everyday routines.
|
||||
Connect to the home network or VPN before opening this service.
|
||||
tags: [home, lights, routines]
|
||||
group: Everyday tools
|
||||
internal_only: true
|
||||
recipes:
|
||||
title: Recipes
|
||||
url: https://recipes.example.com
|
||||
description: Favorite meals, shopping lists and this week's dinner plan.
|
||||
tags: [cooking, meals, shopping]
|
||||
group: Everyday tools
|
||||
calendar:
|
||||
title: Family calendar
|
||||
url: https://calendar.example.com
|
||||
description: Appointments, school events and reminders for bin collection.
|
||||
tags: [calendar, reminders, planning]
|
||||
group: Everyday tools
|
||||
groceries:
|
||||
title: Shopping list
|
||||
url: https://groceries.example.com
|
||||
description: Add groceries before the next trip to the shops.
|
||||
tags: [shopping, groceries, pantry]
|
||||
group: Everyday tools
|
||||
weather:
|
||||
title: Garden weather station
|
||||
url: https://weather.example.com
|
||||
description: Local rainfall and temperature readings from the garden sensors.
|
||||
tags: [weather, garden, sensors]
|
||||
group: Everyday tools
|
||||
energy:
|
||||
title: Energy
|
||||
url: https://energy.example.com
|
||||
description: Household electricity trends and daily solar production.
|
||||
tags: [energy, solar, sensors]
|
||||
group: Everyday tools
|
||||
internal_only: true
|
||||
guides:
|
||||
title: Workshop guides
|
||||
url: https://guides.example.com
|
||||
description: Repair notes and instructions for frequently used tools.
|
||||
tags: [workshop, repair, guides]
|
||||
group: Workshop
|
||||
printer:
|
||||
title: 3D printer
|
||||
url: https://printer.example.com
|
||||
description: Check the current print before starting another job.
|
||||
tags: [printing, workshop, projects]
|
||||
group: Workshop
|
||||
internal_only: true
|
||||
inventory:
|
||||
title: Parts inventory
|
||||
url: https://inventory.example.com
|
||||
description: Find spare cables, screws and electronic components.
|
||||
tags: [parts, storage, workshop]
|
||||
group: Workshop
|
||||
projects:
|
||||
title: Works in progress
|
||||
url: https://projects.example.com
|
||||
description: Sketches, task lists and progress photos for household projects.
|
||||
tags: [projects, planning, workshop]
|
||||
group: Workshop
|
||||
manuals:
|
||||
title: Appliance manuals and warranty notes
|
||||
url: https://manuals.example.com
|
||||
description: Setup instructions and service records for household appliances.
|
||||
tags: [manuals, warranties, repair]
|
||||
group: Workshop
|
||||
router:
|
||||
title: Router
|
||||
url: https://router.example.com
|
||||
description: Manage the home network; leave changes to your household maintainer.
|
||||
tags: [network, router, maintenance]
|
||||
group: Behind the scenes
|
||||
requires_auth: true
|
||||
internal_only: true
|
||||
admin_only: true
|
||||
monitoring:
|
||||
title: Server health
|
||||
url: https://monitoring.example.com
|
||||
description: Storage capacity, temperatures and service availability.
|
||||
tags: [monitoring, maintenance, server]
|
||||
group: Behind the scenes
|
||||
internal_only: true
|
||||
admin_only: true
|
||||
containers:
|
||||
title: Container console
|
||||
url: https://containers.example.com
|
||||
description: Maintenance console for the services running on the home server.
|
||||
tags: [containers, maintenance, server]
|
||||
group: Behind the scenes
|
||||
requires_auth: true
|
||||
internal_only: true
|
||||
admin_only: true
|
||||
dns:
|
||||
title: Local name service
|
||||
url: https://dns.example.com
|
||||
description: Household DNS records and local network filtering settings.
|
||||
tags: [network, dns, maintenance]
|
||||
group: Behind the scenes
|
||||
internal_only: true
|
||||
admin_only: true
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,35 @@
|
||||
# Philosophy
|
||||
|
||||
do one thing. only.
|
||||
do it well. maybe.
|
||||
|
||||
this is a highly opinionated app prioritizing the following principles
|
||||
|
||||
1. simplicity (user experience and developer experience)
|
||||
2. performance (fast enough, but not the fastest)
|
||||
3. low maintenance (this is just 1, but repeated in new words)
|
||||
|
||||
## the one thing
|
||||
|
||||
Directory is a sitemap for the apps you use at home. A browser start page for you,
|
||||
your household, or whoever keeps asking where the photos went.
|
||||
|
||||
## a text file
|
||||
|
||||
Configuration lives in a text file. Edit it, back it up, reverse it and put it
|
||||
in git.
|
||||
|
||||
## no user management
|
||||
|
||||
Everyone sees the same page. Shared sign-in and SSO can control who gets in.
|
||||
|
||||
Keep. It. Simple.
|
||||
|
||||
## not enough?
|
||||
|
||||
you've got options. more features come with more things to configure and maintain.
|
||||
decide how much you want to take on.
|
||||
|
||||
- [dashy](https://dashy.to/) — widgets, themes, and service status checks
|
||||
- [homepage](https://gethomepage.dev/) — service integrations and widgets
|
||||
- your own page — hand code it, vibe code it, hard code and maintain it all
|
||||
@@ -1,52 +1,6 @@
|
||||
# @directory/config
|
||||
|
||||
Shared configuration for Directory. The [contract](../../docs/config.md) lists fields,
|
||||
defaults, paths and environment syntax.
|
||||
Shared server-side YAML loading, validation and path resolution for Directory.
|
||||
|
||||
```ts
|
||||
import { loadConfigFile } from '@directory/config';
|
||||
|
||||
const config = await loadConfigFile('config/directory.yaml', { env: process.env });
|
||||
```
|
||||
|
||||
`loadConfigFile(path?, { env }?)` reads YAML and calls
|
||||
`validateConfig(raw, { env }?)`. Both apply defaults, interpolate string values,
|
||||
and check the canonical TypeBox schema. Loaded `site.logo_path` and
|
||||
`site.favicon_path` are required strings, defaulting independently to bundled
|
||||
`/favicon.svg`. Site images accept that explicit bundle or a strict mounted
|
||||
`/public/` image path; bookmark image fields stay optional and mounted-only.
|
||||
Omitting `env` uses `process.env`; an
|
||||
injected map is used on its own. Neither function watches files or publishes
|
||||
state. The loader returns a fresh private configuration without changing the
|
||||
input. Preparing the browser's directory data and immutable snapshots belongs
|
||||
to the web server.
|
||||
|
||||
Bookmark `tags` accepts only a YAML list of strings and defaults to `[]`, never a
|
||||
CSV scalar. Loaded strings retain their whitespace, case, duplicates and order
|
||||
after environment interpolation. Interpolation works inside list elements, not
|
||||
as a way to turn a scalar into a list; commas inside an element stay literal.
|
||||
The web display trims each tag and drops blank labels without deduplicating,
|
||||
changing case or splitting commas. Migrate old CSV tags to explicit lists manually.
|
||||
|
||||
Input is checked before defaulting: nulls, non-plain or cyclic objects, accessors,
|
||||
hidden/symbol properties and unsafe keys are rejected. Arrays are accepted only
|
||||
where the schema allows lists. This prevents TypeBox object defaults from changing
|
||||
invalid input into valid objects. Fields are never silently removed.
|
||||
|
||||
Failures throw `ConfigError` with a safe message and readonly `issues` containing
|
||||
`code`, `path` and `message`. Values, filenames, variable names and original
|
||||
exceptions are excluded. Known field names remain visible; link IDs and unknown
|
||||
names become `*`. Catch unexpected errors without printing their raw contents.
|
||||
|
||||
The package also exports `resolveConfigPath`, `resolveDeploymentRoot` and
|
||||
`resolvePublicDir`. These resolve operational paths without reading assets.
|
||||
The default layout has sibling `config/` and `public/` directories.
|
||||
|
||||
Directory state, snapshot preparation and reload policy belong to the web server.
|
||||
The shared loader keeps no cached configuration. See the web server's
|
||||
[directory API](../../docs/development.md#prepared-configuration-snapshots).
|
||||
|
||||
Browser code must use **type-only** imports from `@directory/config/types`; the main
|
||||
entry imports filesystem and validation code. `@directory/config/schema.json` is the
|
||||
generated text-editor schema. See [development](../../docs/development.md) for
|
||||
build, schema-drift and test commands.
|
||||
- [Configuration](../../docs/config.md): fields, defaults and safe errors.
|
||||
- [Schema](src/config.schema.json).
|
||||
@@ -401,11 +401,8 @@ describe('YAML file loader', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('validates the minimal production starter with defaults and no auth', async () => {
|
||||
const path = fileURLToPath(
|
||||
new URL('../../../apps/web/config/directory.example.yml', import.meta.url)
|
||||
);
|
||||
const config = await loadConfigFile(path, { env: {} });
|
||||
it('loads minimal YAML with defaults and no auth', async () => {
|
||||
const config = await loadYaml('links: {}\n');
|
||||
expect(config.version).toBe(1);
|
||||
expect(config.links).toEqual({});
|
||||
expect(config).not.toHaveProperty('auth');
|
||||
@@ -423,6 +420,14 @@ describe('YAML file loader', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('validates the downloadable starter without auth or environment values', async () => {
|
||||
const path = fileURLToPath(new URL('../../../docs/directory.example.yaml', import.meta.url));
|
||||
const config = await loadConfigFile(path, { env: {} });
|
||||
expect(config.version).toBe(1);
|
||||
expect(Object.keys(config.links).length).toBeGreaterThan(0);
|
||||
expect(config).not.toHaveProperty('auth');
|
||||
});
|
||||
|
||||
it('loads an empty directory, flat links, multiline descriptions and native booleans', async () => {
|
||||
expect((await loadYaml('links: {}')).links).toEqual({});
|
||||
const config = await loadYaml(`
|
||||
|
||||
Reference in new issue
Block a user