Commit Graph
27 Commits
Author SHA1 Message Date
libretechandClaude Opus 4.7 cdc7f26269 Add tenant-scoped notes REST API
internal/httpapi/notes.go exposes:
- GET    /api/notes            list summaries {id, title, updated_at}
- GET    /api/notes/{id}       full {id, title, content, updated_at}
- PUT    /api/notes/{id}       create/update; ?base=<unix> for
                                optimistic-locking conflict detection
- DELETE /api/notes/{id}       remove; ?base=<unix> guards against
                                deleting a row modified after the
                                client last saw it

Backed by tenant.FS so all reads/writes go through the per-user
sandbox — path traversal is rejected at parse time (regex slug)
and again by os.Root inside the FS layer.

On-disk format is plain Markdown: first line `# Title`, rest is
content. grep / cat / vim still produce a usable view of raw
files. Title round-trips through composeNote/splitTitle.

Conflict semantics: when the client supplies ?base=<unix>, the
server compares against the file's mtime. If the file is newer,
respond 409 with the current note body so the client can present
a merge UI. Same logic on DELETE returns 409 alone.

cmd/librenotes/serve.go grows a tenantPool that memoises FS
handles per user id; defer-closes them on shutdown.

Tests cover: full CRUD round-trip, cross-tenant isolation,
unauthenticated 401s, invalid IDs (regex rejection), and the
conflict path with a real mtime advance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:42:43 +02:00
libretechandClaude Opus 4.7 49ad467aa9 Switch pane resize handle to pointer events
internal/notesium/web/app/pane.js drove the sidebar/pane resize
via mousedown + window-level mousemove/mouseup, which doesn't
fire on touch (browsers only emulate mouse for taps, not drags).

Replaced with pointer events:
- @pointerdown on the handle (covers mouse, touch, pen).
- setPointerCapture so we keep receiving pointermove/pointerup
  events when the pointer drifts off the handle. This eliminates
  the need for document-level listeners and avoids stuck-drag
  states when the user releases outside the window.
- pointermove + pointerup + pointercancel listeners on the
  captured target only — when the capture ends they're removed
  regardless of whether the user is still on top of the handle.
- Filter on event.pointerId so a second simultaneous touch
  (e.g., a multi-finger gesture) cannot hijack the in-progress
  resize.
- event.button !== 0 guard rejects right-click / middle-click.
- touch-action: none on the handle so the browser doesn't try
  to interpret a horizontal drag as a page scroll.

CodeMirror's internal mousedown handlers in note.js / preview.js
are left alone — those are link-click guards, not drags, and
CodeMirror's own pointer support handles touch internally.

Closes #20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:30:43 +02:00
libretechandClaude Opus 4.7 e6d3893308 Overhaul CSS for 320px–2560px viewports
style.css now has explicit breakpoints and primitives covering
the full target range:

- Global: overflow-x: hidden on body, max-width:100% on media,
  fluid typography via clamp() so headings shrink on 320px.
- .wrap: 64rem cap at desktop, 72rem at 1440px, 96rem at 2560px;
  generous side padding at large widths so text doesn't hug the
  edge on huge monitors.
- .app-shell layout primitive (grid: sidebar + content [+ aside
  on ultrawide]) ready for the eventual notes UI:
    * mobile: single column, sidebar hidden behind a toggle
      ([data-sidebar="open"] reveals it).
    * 768px+: 2-column with 16rem sidebar.
    * 1024px+: 18rem sidebar.
    * 1440px+: 20rem sidebar, content max-width 56rem so reading
      lines don't grow unbounded.
    * 2560px+: 3-column (sidebar | content | aside) so the
      editor stays at reading width while the extra real estate
      hosts backlinks/preview.
- .app-resize-handle with touch-action:none so pointer-event
  drag handlers won't conflict with browser scrolling.
- Auth card tightens on viewports under 360px.

Result: no horizontal scroll at any width; content uses ultrawide
space effectively without sacrificing legibility.

Closes #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:28:19 +02:00
libretechandClaude Opus 4.7 7c3b40c963 Add PWA manifest, service worker, and install prompt
- manifest.webmanifest: standalone display mode, theme #2563eb,
  start_url=/app.html (so users who install land in the app
  shell, not the marketing page), scope=/. Three icons: 192px
  any-purpose, 512px any-purpose, 512px maskable for adaptive
  icons on Android.
- icons/: PNGs generated from favicon.svg.
- sw.js: cache-first for the precached app shell, network-first
  for /api/* and /auth/* (we never serve stale auth or notes).
  Versioned cache name (librenotes-shell-v1) so a SW update
  evicts old assets. skipWaiting + clients.claim so a new SW
  takes over without a manual reload.
- pwa.js: registers the SW on every page and handles
  beforeinstallprompt by showing #install-btn. Hides the button
  again on appinstalled. Defer loaded so it never blocks render.
- All HTML pages link the manifest, set the theme-color meta,
  and load pwa.js. Landing page exposes the install button next
  to the existing CTAs.

Closes #19.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:27:32 +02:00
libretechandClaude Opus 4.7 274c7054d0 Add JWT session client and tenant-scoped storage
cmd/librenotes/web/public/auth-client.js exposes window.authClient
with the full session API used by the rest of the frontend:

Session storage (#14):
- saveSession / loadSession / clearSession / isAuthenticated
- Backed by sessionStorage, not localStorage: tokens are isolated
  per tab and cleared on tab close. localStorage would survive
  tab close on a shared device, which we want to avoid.
- loadSession returns null when expires_at has passed, so callers
  treat expired sessions as logged-out without a network round
  trip.

API wrapper (#14):
- apiFetch(url, init) attaches Authorization: Bearer <jwt> to
  every call. On 401 it clears the session and redirects to
  /login.html?next=<current-path> so the user returns where they
  started. Throws after the redirect so the caller's .then does
  not run with stale data.

Tenant-scoped localStorage (#15):
- tenantStore() returns a get/set/remove wrapper whose keys are
  prefixed "librenotes:{user_id}:". Two users on the same browser
  therefore have fully independent UI state. JSON serialisation
  with try/catch fallbacks for corrupted or quota-exceeded
  storage so a bad blob never crashes the app.
- clearTenantStore(userID) removes every key with that prefix.
  Called from clearSession() so logout wipes both the JWT and
  the user's preferences.

verify.html + verify.js complete the magic-link flow: read
?token=, POST /auth/verify, hand the response to saveSession(),
strip the token from the URL via history.replaceState. Errors
route the user back to /login.html.

app.html + app.js are a minimal authenticated landing demonstrating
the full stack end-to-end: apiFetch hits /api/whoami, tenantStore
persists a theme preference, logout clears both. The full notes
UI is left to a later phase — this is the seam.

Closes #14 and #15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:25:07 +02:00
libretechandClaude Opus 4.7 dc5a08e682 Add magic-link login UI with client-side validation
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>
2026-04-28 22:24:48 +02:00
libretechandClaude Opus 4.7 ee4de51728 Add librenotes serve command and public landing page
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>
2026-04-28 22:24:37 +02:00
libretechandClaude Opus 4.7 db3b6c1b5a Add tenant-aware HTTP middleware and router
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>
2026-04-28 22:19:09 +02:00
libretechandClaude Opus 4.7 c9b8c4445b Add per-tenant filesystem isolation
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>
2026-04-28 22:17:38 +02:00
libretechandClaude Opus 4.7 d9f3574913 Implement email magic-link authentication
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>
2026-04-28 22:16:25 +02:00
libretechandClaude Opus 4.7 0924e3cee9 Add user model and SQLite storage
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>
2026-04-28 22:13:28 +02:00
libretechandClaude Opus 4.7 b409519661 Add reproducible dev environment
- 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>
2026-04-28 21:58:59 +02:00
libretechandClaude Opus 4.7 a36fc1c8cc Add Gitea Actions CI workflow
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>
2026-04-28 21:55:40 +02:00
libretechandClaude Opus 4.7 72454f08ab Add Makefile with build, test, lint, run, and clean targets
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>
2026-04-28 21:55:26 +02:00
libretechandClaude Opus 4.7 dc6ef99c3a Add README with librenotes branding and build instructions
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>
2026-04-28 21:53:28 +02:00
libretechandClaude Opus 4.7 42fac0ab33 Document fork relationship with Notesium upstream
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>
2026-04-28 21:53:04 +02:00
libretechandClaude Opus 4.7 094250609c Fork Notesium source and restructure into Go package layout
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>
2026-04-28 21:52:25 +02:00
libretechandClaude Sonnet 4.5 aee9086633 Fix Gitea pipelines to use authenticated tea CLI without --login flag
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>
2026-02-25 19:15:33 +01:00
libretechandClaude Opus 4.6 3e10fde0e1 Add CLAUDE.md with project conventions and tool preferences
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:11:46 +01:00
libretechandClaude Opus 4.6 1230c6a538 Add Gitea issue pipelines and prompts using tea CLI
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>
2026-02-25 17:02:48 +01:00
libretechandClaude Opus 4.6 22370827ee Add GitHub issue pipelines and prompts using gh CLI
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>
2026-02-25 17:02:42 +01:00
libretechandClaude Opus 4.6 fc24f9a8ab Add Wave general-purpose pipelines
ADR, changelog, code-review, debug, doc-sync, explain, feature,
hotfix, improve, onboard, plan, prototype, refactor, security-scan,
smoke-test, speckit-flow, supervise, test-gen, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:36 +01:00
libretechandClaude Opus 4.6 bfbb7c87ad Add Gitea issue personas using tea CLI
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>
2026-02-25 17:02:24 +01:00
libretechandClaude Opus 4.6 58a9bd394c Add GitHub issue personas using gh CLI
Analyst, commenter, and enhancer personas for GitHub issue
pipelines via the gh CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:19 +01:00
libretechandClaude Opus 4.6 8233d4fdd7 Add Wave base personas for pipeline agents
Core persona definitions: auditor, craftsman, debugger, implementer,
navigator, philosopher, planner, researcher, reviewer, summarizer,
supervisor, synthesizer, validator, and others.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:14 +01:00
libretechandClaude Opus 4.6 59411ede0f Add Wave contract schemas for pipeline validation
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>
2026-02-25 17:02:07 +01:00
libretechandClaude Opus 4.6 3a74a298a5 Initial project setup with Nix flake and gitignore
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>
2026-02-25 17:02:01 +01:00