Opening a pond landed on whatever sorted first in the sidebar — stable,
but a rule nobody could see, and one whose target moved as soon as
someone added a page ahead of it. New ponds landed on the empty-pond hint
instead of anything useful.
- `startPageId` joins the pond settings. No migration: `Pond.settings` is
already jsonb. It stores an id, not a slug, so renaming or moving the
page keeps it working.
- `PondHomePage` prefers it, but only when the page is in this user's
page list. That list already holds just what they may see, so a start
page hidden by a page-scoped grant — or trashed — falls back silently
instead of landing them on a 404, and it costs no extra request.
- Both creation paths give the pond a start page, titled from the
creator's stored locale. It happens after the creating transaction
commits: the owner's grant is written inside it and permissions cache
per pond, so creating the page any earlier would ask about rights the
grant has not published yet. A failure is logged, not fatal — a pond
without a start page still works.
`PagesModule` imported `PondsModule` without using it. Removing that
vestigial edge let PondsModule depend on PagesModule in the honest
direction instead of tying the two together with forwardRef.
Every pond created through the api now owns a page, which broke eight
suites whose teardown deleted ponds directly — `Page.pond` deliberately
has no cascade, because a real purge removes contents explicitly and
audits it. A shared `deletePondsWhere` helper deletes pages first. Two
tests that counted pages now account for the start page rather than
pretending the pond began empty.
Every route now declares its access rule explicitly and is enforced
through the shared resolution algorithm (permissions.md):
- PermissionGuard + decorators (@RequiresPondRole, @RequiresPagePermission,
@RequiresAttachmentPermission, @AuthenticatedOnly) applied to every
route; a route-enumeration test proves full coverage alongside
@Public()/Site-Admin-guarded routes.
- 404/403 policy (documented in README conventions): denied reads answer
404 (existence hiding), denied writes on readable things answer 403;
trash views need write capability (ADR 0013).
- PermissionService resolves page/pond questions via the shared resolver,
with an in-process pond-context cache (grants + label parents) that is
invalidated on every grant/label-tree change and TTL-bounded as a
multi-process safety net. Grant changes also fire pond_access_changed
for collab revalidation (#39/#53).
- shared: pond-scope resolution (hasPondRole, canSeePond) next to the
page resolver; grant wire schemas + GrantView.
- Owner Pond-Admin grants: migration backfill for all existing ponds,
created transactionally with every new pond (shared + personal + seed).
- Grant CRUD under /ponds/:id/grants (pond_admin-gated) with structural
and referential validation, last-admin protection, audit logs.
- InterimAccessService deleted; page lists, search, backlinks, phantom
links, and trash listings are filtered per page through the resolver;
collab tokens are now truly ro for readers.
- Fixture-matrix e2e (reader/editor/pond admin/foreign, label-deny,
authenticated-subject, revoke-then-immediate-deny cache test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
Prisma models per data-model.md: users (status enum, site-admin flag),
user_identities (password provider now, OIDC later — subject is the
stable user id), sessions (hashed ids), auth_tokens (hashed, single-
use), plus rate_limits and mail_outbox for the upcoming M1 stories.
UsersService creates accounts transactionally with Argon2id-hashed
password identities (OWASP parameters, rehash detection) and maps
uniqueness violations to field-level conflicts. Database-backed suites
run when TEST_DATABASE_URL is set — locally against the dev db, in CI
via a new postgres service container; shared auth schemas (username,
password policy incl. common-password blocklist) ship with tests.
Closes#10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>