No description
Find a file
Arlind Ukshini a2cbf57ce2 stack-scene: centered title, per-card counters, earlier anchor
- Stack title bar moves from top-left next to the site-mark to
  centered at ~14vh so the title anchors visually to the cards
  below. Font size bumped to clamp(2rem, 3.6vw, 3rem).
- Counter ("1 / 4 … 4 / 4") relocates from the title bar into
  each .layer-card as a .card-counter element in the top-right of
  each card-box. No longer driven by ScrollTrigger onUpdate —
  each card carries its own number, so stacked + grid phases
  both read correctly without JS. Grid phase shrinks the counter
  so it doesn't compete with the per-cell label.
- SCENE_ANCHOR_OFFSET for stack-scene drops from 1800 back to 0,
  so clicking the "Capabilities" dot lands at the top of the
  pin — the title and first card come in together instead of
  starting mid-stack.

Welcome step: the "desktop experience" aside and its CSS are
removed. Users now see only the two definitions before the CTA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:46:08 +02:00
.claude small changes 2026-04-23 14:02:07 +02:00
admin /fenjaops: drop masthead subtitle 2026-04-23 18:08:16 +02:00
bin add hidden /fenjaops admin page (read-only) + is_admin invite flag 2026-04-23 17:29:19 +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 stack-scene: centered title, per-card counters, earlier anchor 2026-04-24 10:46:08 +02:00
public stack-scene: centered title, per-card counters, earlier anchor 2026-04-24 10:46:08 +02:00
src add hidden /fenjaops admin page (read-only) + is_admin invite flag 2026-04-23 17:29:19 +02:00
.env.example simplify login and update features 2026-04-23 10:38:37 +02:00
.gitignore Initial commit: project-bifrost auth + timeline 2026-04-22 14:39:16 +02:00
AUTH_SIMPLIFICATION_NOTES.md simplify login and update features 2026-04-23 10:38:37 +02:00
CHANGES 2.md add welcome dot, card shrunk, topo.paralx., sticky scroll 2026-04-23 12:06:07 +02:00
CHANGES 3.md small fixes 2026-04-23 12:41:18 +02:00
CHANGES 4.md fix hero placement, add iamges 2026-04-23 13:31:11 +02:00
CHANGES.md remove archive and other changes 2026-04-23 11:40:44 +02:00
CHECKLIST.md add mobile view at protected/mobile/ (UA-dispatched) 2026-04-24 10:03:13 +02:00
CLAUDE.md add mobile view at protected/mobile/ (UA-dispatched) 2026-04-24 10:03:13 +02:00
INSTALL.md update docs: minimal env, WSL deploy, join tracking, rsync excludes 2026-04-23 17:10:08 +02:00
MERGE_NOTES.md Merge Project Bifrost scenes into the Overview page 2026-04-22 17:48:44 +02:00
OPERATIONS.md /fenjaops: admin-only form to invite non-admin users 2026-04-23 18:07:47 +02:00
package-lock.json simplify login and update features 2026-04-23 10:38:37 +02:00
package.json simplify login and update features 2026-04-23 10:38:37 +02:00
PROJECT.md add mobile view at protected/mobile/ (UA-dispatched) 2026-04-24 10:03:13 +02:00
README.md /fenjaops: admin-only form to invite non-admin users 2026-04-23 18:07:47 +02:00
server.js add mobile view at protected/mobile/ (UA-dispatched) 2026-04-24 10:03:13 +02:00
SITE_STRUCTURE_REFERENCE.md update docs 2026-04-23 15:00:53 +02:00

project-bifrost

Invite-only Fenja AI entrance and archive. Node/Express + SQLite, behind Nginx on a VPS. Auth is email-only against an invite list (no SMTP, no one-time codes).

Local development (Windows / VS Code)

# 1. Install deps
npm install

# 2. Configure
copy .env.example .env
# .env is minimal — PORT, PUBLIC_ORIGIN, NODE_ENV only. Leave
# NODE_ENV=development locally so cookies work over HTTP.

# 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 → session cookie issued, welcome step renders
#    c) Click "Learn more" → lands on /timeline (the authed home page)
#    d) Hit http://127.0.0.1:3000/timeline 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 /etc/fenja/env (kept out of /opt/fenja/ so it can't be rsynced over). The file is minimal — see .env.example for the three values (PORT, PUBLIC_ORIGIN, NODE_ENV=production):

sudo nano /etc/fenja/env
sudo chown root:fenja /etc/fenja/env
sudo chmod 640 /etc/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/timeline
# 302 → /  (gate holds without a session cookie)

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

curl -i -X POST https://project-bifrost.fenja.ai/auth/login \
     -H 'Content-Type: application/json' \
     -d '{"email":"nobody@example.com"}'
# 403 + {"error":"not_invited"}  (invite-list-only; enumeration is acceptable by design)

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

# Admin flag (gates the hidden /fenjaops page). Grant/revoke is CLI-only.
sudo -u fenja node /opt/fenja/bin/invite.js admin add    someone@example.com
sudo -u fenja node /opt/fenja/bin/invite.js admin remove someone@example.com
sudo -u fenja node /opt/fenja/bin/invite.js admin list

Admins can also add non-admin invites from the hidden /fenjaops page (see OPERATIONS.md). Admin promotion and invite removal stay on the CLI by design — see PROJECT.md §Non-negotiable properties.

Reading Join-CTA clicks

Every press of the final "Join Project Bifrost" CTA is logged to the bifrost_joins table. Read it with:

sudo -u fenja node /opt/fenja/bin/joins.js list              # every click, newest first
sudo -u fenja node /opt/fenja/bin/joins.js summary           # one row per user, with counts
sudo -u fenja node /opt/fenja/bin/joins.js for someone@example.com
sudo -u fenja node /opt/fenja/bin/joins.js stats             # totals + unique users

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
│   └── joins.js              # CLI: read the Join-CTA click log
├── src/
│   ├── db.js                 # SQLite schema + prepared statements
│   ├── sessions.js           # opaque 256-bit session cookies
│   ├── middleware.js         # rateLimit, requireAuth
│   └── auth.js               # /auth/login, /auth/logout, /auth/me
├── public/                   # served to anyone
│   ├── entrance.html         # the Entrance page (email form → welcome)
│   └── entrance.js           # entrance form behaviour
├── protected/                # served only with a valid session cookie
│   ├── index.html            # the timeline (authed home page)
│   ├── timeline.js           # horizontal timeline + dot-nav + globe
│   └── bifrost.js            # Overview page scroll scenes
├── admin/                    # served at /fenjaops, gated by requireAuth+requireAdmin
│   ├── index.html            # stats + invite list + join log + "Invite a user" form
│   ├── admin.css
│   └── admin.js
├── 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 gated pages. 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. Secure is conditional on NODE_ENV=production.
  • Sessions are opaque 256-bit random IDs stored in SQLite. Revoking is a DELETE.
  • Auth is invite-list-only. POST /auth/login with an email on the list issues a session; an email not on the list returns 403 {error:"not_invited"}. There are no one-time codes, no pepper, no SMTP — the invite list is the auth factor. Enumeration is acceptable by design (preview content).
  • Rate limit: 30 login attempts per IP per hour.
  • 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.