cmd/librenotes/web/public/login.{html,js}:
- Email input with required + autocomplete + autofocus, ARIA
attributes for screen readers (aria-describedby, aria-invalid,
role="alert" on the error container, role="status" on success).
- Client-side regex validation runs before POST to /auth/login
to avoid a network round-trip for obvious typos. Server is
still the source of truth.
- Loading state disables the button and changes its label.
- Success state replaces the form with "Check your email"
including the address, plus the 15-minute / single-use note.
- Error states map server statuses to user-friendly messages:
429 -> "too many requests", 400 -> "invalid email", anything
else -> generic server error. Network errors get their own
message so users can distinguish offline from server problems.
- No external CSS or JS dependencies; works with keyboard and
on small viewports.
Closes#13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cmd/librenotes/serve.go wires the multi-tenant HTTP server:
storage + auth + httpapi packages, configurable via flags or
LIBRENOTES_* env vars. Embeds web/public/ for unauthenticated
static content. Generates an ephemeral JWT secret with a warning
when none is supplied. Adds security headers (CSP, nosniff,
DENY-frame, no-referrer) on every response. Background goroutine
purges expired magic-link tokens every 10 minutes.
cmd/librenotes/web/public/ provides the unauthenticated frontend:
- index.html: hero, features grid, fork attribution, footer.
Mobile-first, responsive from 320px up via clamp() and
auto-fit grid. SEO + Open Graph tags. No JS dependency.
- privacy.html: placeholder privacy policy (full text TBD).
- style.css: shared design tokens (light/dark via [data-theme]),
used by landing, auth pages, and the post-login app shell.
- favicon.svg: minimal mark.
The "serve" command sits alongside the original notesium CLI
verbs; main.go dispatches "serve" to the new code path and
forwards everything else to notesium.Run().
Closes#16.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
internal/httpapi/ provides:
- Tenant{UserID, Email} carried on context.Context, with
WithTenant / TenantFrom helpers and ErrNoTenant for the
programming-error case (route reached without middleware).
- AuthMiddleware verifies an Authorization: Bearer <jwt> on every
request via auth.Signer.Verify (which already enforces HS256
and rejects alg=none). On failure: 401, with the underlying
reason logged server-side but not exposed to the client.
- RequireTenantOwnership(ownerID) compares the request's tenant
against the resource owner; returns 403 on mismatch. Handlers
that touch tenant-owned resources call this guard.
- Server.Routes() mounts /auth/* unauthenticated and wraps
/api/* with the middleware. /api/whoami is included as the
canonical example of a tenant-scoped endpoint.
Tests cover: valid JWT pass-through, missing/empty Authorization,
wrong scheme, malformed JWT, tampered signature, JWT signed with
a different secret (cross-tenant key confusion), and the 200/403
matrix for RequireTenantOwnership.
Closes#11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
internal/tenant/ provides FS, a sandboxed handle for a single
tenant's notes directory. Implementation strategy:
- Defence in depth: every relative path is validated up front
(rejects "..", absolute paths, NUL bytes, empty), then handed
to os.Root (Go 1.24+) which enforces the boundary at the
syscall layer using openat(2)+RESOLVE_BENEATH on Linux. This
closes TOCTOU races and symlink-target swapping.
- WriteFile is atomic (write to .tmp, rename in-root). Mode 0o600
on files, 0o700 on directories. Tenant root is created with
0o700 by Open().
- Errors are normalised: fs.ErrNotExist -> ErrNotFound, anything
os.Root rejects as "outside" the root -> ErrInvalidPath. The
HTTP layer can map cleanly to 404 / 400.
Tests cover the full traversal attack surface — "../", absolute
paths, mixed separators, NUL bytes, "." and "" — plus symlink
escapes and cross-tenant isolation. All vectors return errors;
none escape the root.
Closes#10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
internal/auth/ provides:
- TokenStore: 32-byte cryptographically random one-time tokens.
Only the SHA-256 hash is persisted (so a DB leak doesn't grant
active sessions). Comparison uses subtle.ConstantTimeCompare.
Single-use is enforced via UPDATE ... WHERE used_at IS NULL.
- Signer: HS256 JWTs with 24h lifetime, jwt.WithValidMethods to
reject alg=none and other downgrade attacks.
- LogMailer (dev) and SMTPMailer (prod via net/smtp) behind a
Mailer interface.
- RateLimiter: DB-backed fixed window per email; default 5 per
15 min for the magic-link flow.
- Service: orchestrates RequestLogin (auto-creates user on first
login, generates token, emails magic link) and Verify (consumes
token, updates last_login, issues JWT).
- Handlers: POST /auth/login and GET/POST /auth/verify.
HandleLogin returns 202 even on validation failure to avoid
account enumeration; rate-limit hits surface as 429.
Schema additions: magic_tokens (with FK + cascade) and
login_attempts. UserStore.SetStoragePath added for completeness.
Tests cover: token issue/consume, single-use, expiry, rate limit,
JWT round-trip, alg=none rejection, signature tampering, purge,
HTTP handlers (login + verify, missing/invalid token paths).
Closes#9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
internal/storage/ provides:
- Open(path) to create or open the SQLite database with WAL journal,
busy timeout, and foreign keys enabled
- Embedded migrations that create the users table on first run
- UserStore with Create, GetByID, GetByEmail, UpdateLastLogin, Delete
- Email normalisation (trim+lowercase) and uniqueness enforcement
with ErrEmailTaken
- ErrNotFound on lookups and deletes
- UUIDv4 IDs auto-generated when caller leaves ID empty
Uses modernc.org/sqlite (pure-Go) so the binary stays CGO-free and
matches Dockerfile.dev's CGO_ENABLED=0.
Tests cover all CRUD operations, email uniqueness (case-insensitive),
WAL mode verification, and ErrNotFound paths.
Closes#8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- flake.nix: rebrand description, add Go 1.25, gopls, gotools,
staticcheck, golangci-lint, gnumake to all dev shells. Add a
plain `dev` shell (`nix develop .#dev`) that does not wrap the
shell in the bubblewrap sandbox so contributors can use a
standard Go toolchain.
- Dockerfile.dev: golang:1.22-bookworm with make, git, gopls and
staticcheck, /workspace as default cwd. CGO disabled.
- README: document both nix and Docker dev paths.
flake.lock is committed for reproducibility.
Closes#6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runs on push to main and pull requests against main:
- go mod download + verify
- make lint (go vet)
- make build
- make test (race detector)
Uses actions/setup-go@v5 with built-in module caching, Go 1.22.
Workflow times out at 5 minutes per the acceptance criteria.
Closes#5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standard targets:
- build: compiles cmd/librenotes with version/buildtime ldflags
- test: race detector enabled, full module
- lint: go vet, plus staticcheck if available
- run: build + execute, ARGS forwarded
- clean: remove binary and test/coverage artifacts
Variables (BINARY, OUTDIR, GO, GOFLAGS, LDFLAGS, TESTFLAGS) are
overridable so the CI workflow (#5) can invoke targets with
custom output paths or flags.
Closes#38.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the upstream Notesium README with librenotes-specific
content: project description, multi-tenant goals, build/run
instructions referencing cmd/librenotes, Nix-based dev setup,
fork attribution, and MIT license note.
CI badge points at the workflow that #5 will create. Module path
and directory layout match the structure landed in the previous
fork commit.
Closes#37.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LICENSE retains the original Notesium copyright alongside librenotes.
NOTICE records the upstream URL, fork commit hash
(aff9f460c2d864112db7f0935b4168b107289d91), fork date, and
instructions for contributors who want to add the upstream remote
and cherry-pick patches.
Closes#36.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial fork of github.com/alonswartz/notesium into librenotes:
- Source moved to internal/notesium/ (package notesium)
- Thin entry point at cmd/librenotes/main.go
- Module renamed to git.librete.ch/public/librenotes
- main() exposed as notesium.Run()
- LICENSE preserved (MIT), NOTICE added with attribution
- Web assets and completion.bash co-located with embedding code
to satisfy go:embed path constraints
Closes#3, #34, #35.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove redundant --login librete flags from all gt-* pipeline tea commands since
authentication is already configured via tea logins. This simplifies the commands
and prevents potential authentication issues.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
gt-issue-impl, gt-issue-research, gt-issue-rewrite, gt-issue-update
pipelines with corresponding prompts. Mirrors the gh-issue-* variants
but uses tea CLI with --login librete for Gitea authentication.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gh-issue-impl, gh-issue-research, gh-issue-rewrite, gh-issue-update
pipelines with corresponding prompts for fetch-assess, plan,
implement, and create-pr steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Analyst, commenter, and enhancer personas for Gitea issue
pipelines via the tea CLI with --login librete auth.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JSON Schema definitions for all pipeline handover contracts
including issue analysis, research, enhancement, and sync flows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nix devshell with gh, bubblewrap sandbox, and yolo mode.
Gitignore for .claude, .wave internals, secrets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>