No description
Find a file
Arlind Ukshini f2f0f8a43e Merge Project Bifrost scenes into the Overview page
Six scroll-bound scenes (hero, architecture stack, words fly-in,
aurora arc, treasure-map, join CTA) now live inside page-overview,
above the existing 23-headline timeline. The Europe map stays as a
static background that fades with scroll.

- protected/index.html: rewrote #page-overview only; timeline and
  archive sections unchanged. Site-2 palette re-mapped to site-1
  Nordic Editorial tokens, Fraunces to Newsreader, tokens scoped
  to #page-overview.
- protected/timeline.js: dot-nav boots window.__bifrost.init()
  on first Overview activation. Added .js class on documentElement.
- protected/bifrost.js (new): Lenis + ScrollTrigger wired to the
  overview's internal scroller via scrollerProxy; drives Europe
  map opacity on scroll.
- protected/vendor/{lenis,gsap,scrolltrigger}.min.js (new):
  extracted from site-2's inlined vendor blobs; CSP-compliant.
- protected/fenja/illustrations/{community,council,pilot}.svg
  (new): treasure-map stop images.

No changes to src/, server.js, deploy/, or public/. CSP stays
strict (script-src 'self'); zero inline scripts added. Auth gate
and session model untouched.
2026-04-22 17:48:44 +02:00
bin Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
data Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
deploy Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
protected Merge Project Bifrost scenes into the Overview page 2026-04-22 17:48:44 +02:00
public add welcome page and change transition to timeline 2026-04-22 17:31:45 +02:00
src Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
.env.example Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
.gitignore Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
CHECKLIST.md Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
CLAUDE.md Add CLAUDE.md with architecture and security invariants for AI agents 2026-04-22 15:31:18 +02:00
INSTALL.md Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
MERGE_NOTES.md Merge Project Bifrost scenes into the Overview page 2026-04-22 17:48:44 +02:00
OPERATIONS.md Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
package-lock.json Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
package.json Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
PROJECT.md Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
README.md Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
server.js add welcome page and change transition to timeline 2026-04-22 17:31:45 +02:00

project-bifrost

Invite-only Fenja AI entrance and archive. Node/Express + SQLite + SMTP, behind Nginx on a VPS.

Local development (Windows / VS Code)

# 1. Install deps
npm install

# 2. Configure
copy .env.example .env
# Edit .env:
#   CODE_PEPPER  — generate with: openssl rand -hex 32
#                  (or any 64-char hex string; use the same one in prod)
#   SMTP_*       — your relay credentials
#   MAIL_FROM    — e.g. "Fenja AI <noreply@project-bifrost.fenja.ai>"

# 3. Invite yourself
node bin/invite.js add your@email.com

# 4. Run
npm run dev
# → http://127.0.0.1:3000

# 5. Walk the flow:
#    a) Open http://127.0.0.1:3000
#    b) Enter your invited email → receive code by mail
#    c) Type code → lands on /archive
#    d) Hit http://127.0.0.1:3000/archive directly in a private window:
#       should redirect to / (proves gating works)

Deploying to the VPS

This assumes:

  • VPS with Nginx already running and TLS for project-bifrost.fenja.ai
  • Node 20+ installed
  • A fenja system user owns /opt/fenja

First-time setup

# On the VPS
sudo useradd -r -s /usr/sbin/nologin fenja
sudo mkdir -p /opt/fenja
sudo chown -R fenja:fenja /opt/fenja

Pushing code from Windows

From the project root, sync with rsync (via WSL) or scp/WinSCP:

# Example rsync (requires WSL or rsync on Windows)
rsync -avz --delete `
  --exclude node_modules --exclude data --exclude .env --exclude .git `
  ./ user@vps:/opt/fenja/

Then on the VPS:

cd /opt/fenja
sudo -u fenja npm ci --omit=dev
sudo chmod 750 /opt/fenja/protected
sudo chmod 750 /opt/fenja/data

Create /opt/fenja/.env in place (do NOT rsync it — keep secrets off your laptop):

sudo -u fenja nano /opt/fenja/.env
sudo chmod 600 /opt/fenja/.env

Install the systemd unit and Nginx config:

sudo cp deploy/fenja.service /etc/systemd/system/fenja.service
sudo systemctl daemon-reload
sudo systemctl enable --now fenja
sudo systemctl status fenja          # should be "active (running)"

sudo cp deploy/nginx.conf /etc/nginx/sites-available/project-bifrost
sudo ln -sf /etc/nginx/sites-available/project-bifrost \
            /etc/nginx/sites-enabled/project-bifrost
# Also ensure the rate-limit zone is declared — see deploy/nginx.conf for the one-liner
sudo nginx -t
sudo systemctl reload nginx

Verifying from outside

curl -I https://project-bifrost.fenja.ai/
# 200 + HTML (the entrance)

curl -I https://project-bifrost.fenja.ai/archive
# 302 → /

curl -I https://project-bifrost.fenja.ai/protected/archive.html
# 404 (this URL literally does not exist)

curl -i -X POST https://project-bifrost.fenja.ai/auth/request-code \
     -H 'Content-Type: application/json' \
     -d '{"email":"nobody@example.com"}'
# 200 + {"ok":true}  (no mail is actually sent; endpoint doesn't leak)

Managing invites on the server

sudo -u fenja node /opt/fenja/bin/invite.js add   someone@example.com
sudo -u fenja node /opt/fenja/bin/invite.js list
sudo -u fenja node /opt/fenja/bin/invite.js remove someone@example.com

Backups

Add a cron job:

# /etc/cron.d/fenja-backup
0 3 * * * fenja sqlite3 /opt/fenja/data/fenja.sqlite ".backup /opt/fenja/data/backup-$(date +\%F).sqlite"
0 4 * * * fenja find /opt/fenja/data -name 'backup-*.sqlite' -mtime +14 -delete

.backup is safe on a live SQLite DB. cp is not.

Project layout

project-bifrost/
├── package.json
├── server.js                 # entry point — binds 127.0.0.1:3000
├── .env.example              # copy to .env
├── .gitignore
├── bin/
│   └── invite.js             # CLI: add / remove / list invites
├── src/
│   ├── db.js                 # SQLite schema + prepared statements
│   ├── sessions.js           # codes, cookies, HMAC
│   ├── mail.js               # Nodemailer SMTP transport
│   ├── middleware.js         # rateLimit, requireAuth
│   └── auth.js               # /auth/* router
├── public/                   # served to anyone
│   └── index.html            # the Entrance page
├── protected/                # served only with a valid session cookie
│   └── archive.html          # placeholder for now; drop the real page here
├── data/                     # created on first run — SQLite lives here
└── deploy/
    ├── nginx.conf
    └── fenja.service

Security model at a glance

  • Static HTML is never served from a public directory for /archive. Node checks the session cookie before reading the file off disk. No cookie → 302 → / (the file bytes never leave the server).
  • The session cookie is HttpOnly, Secure, SameSite=Lax. JavaScript on the page cannot read or steal it.
  • Sessions are opaque 256-bit random IDs stored in SQLite. Revoking is a DELETE.
  • One-time codes are HMAC'd with a server-side pepper (CODE_PEPPER) before storage. A DB leak alone cannot reveal codes.
  • Rate limits on everything. 5 code requests per IP per hour, 20 verify attempts per IP per hour, 5 wrong guesses per code before it's nuked.
  • /auth/request-code always returns 200 — attackers cannot probe the invite list by email-enumeration.
  • Node binds to 127.0.0.1 only. Nginx is the single ingress; there is no public Node port.
  • Strict CSP blocks inline scripts and foreign origins from the gated pages.