Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ae781f5
docs(roadmap): add Phase 07 titanium-licensing auth cutover
finedesignz May 25, 2026
a369806
docs(07): lock context for titanium auth cutover phase (architect-tem…
finedesignz May 25, 2026
05f6f25
docs(07): rename new session table to auth_sessions (collides with Cl…
finedesignz May 25, 2026
f961f4c
docs(07): research for titanium auth cutover
finedesignz May 25, 2026
eebde5c
docs(07): file pattern map for titanium auth cutover
finedesignz May 25, 2026
4c31f0b
docs(07): plan A — titanium client foundation
finedesignz May 25, 2026
128993a
docs(07): plan B — DB schema migration (additive)
finedesignz May 25, 2026
6c8ec20
docs(07): plan C — login + session swap
finedesignz May 25, 2026
48dabea
docs(07): plan D — license gating middleware
finedesignz May 25, 2026
87dd290
docs(07): plans E (migration script) + F (web UI swap)
finedesignz May 25, 2026
762da9e
docs(07): plans G (security hardening) + H (cleanup)
finedesignz May 25, 2026
77db944
docs(07): plans I (testing+rollout) + J (TEMPLATE.md extraction)
finedesignz May 25, 2026
82d3c14
docs(07): plan-checker verdict — accept, no revisions
finedesignz May 25, 2026
8f29ed0
chore(07): add titanium env keys for keygen product+policy
finedesignz May 26, 2026
375d385
feat(07-B): additive schema migration + dal for titanium auth
finedesignz May 26, 2026
d0b764f
feat(07-A): titanium-client foundation (jose JWKS + EdDSA verify + re…
finedesignz May 26, 2026
11615be
feat(07-D): license gating middleware + optional webhook receiver
finedesignz May 26, 2026
dd749a1
feat(07-C): login + opaque sessions + csrf + re-auth + dual-auth shim
finedesignz May 26, 2026
3e0e1f8
feat(07-E): user migration script (titanium subject linking + magic-l…
finedesignz May 26, 2026
c066be0
feat(07-G): security hardening (rate limits + headers + jti hard-fail…
finedesignz May 26, 2026
a321578
feat(07-F): magic-link login UI + csrf header + license badge
finedesignz May 26, 2026
31b7866
docs(07-I): test matrix + rollout runbook + integration smoke
finedesignz May 26, 2026
603bb49
docs(07-J): reusable titanium auth cutover template
finedesignz May 26, 2026
62f4402
chore(07-H): dead-code audit + docs + license-status endpoint
finedesignz May 26, 2026
1cdb1fc
docs: sync openapi for phase 07 endpoints
finedesignz May 26, 2026
72787fc
chore(07): provision keygen licenses for prod users
finedesignz May 26, 2026
604c0fd
merge: origin/main into phase-07 — resolve schema + App + Layout + WS…
finedesignz May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,53 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/remocode
# JWT_SECRET — used by the LEGACY auth path (gated behind ALLOW_LEGACY_LOGIN below)
# and by the GitHub App helper. Will be removed in Phase 07.5 once
# ALLOW_LEGACY_LOGIN is gone and GitHub App helper is rewired.
JWT_SECRET=change-me-in-production-min-32-chars
PORT=3040
HUB_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3040

# ─── Titanium Licensing Auth Cutover (Phase 07) ──────────────────────────────
# Keygen tenant for Titanium Licensing. Public-ish — safe in git (the secret
# bit is the portal token below).
TITANIUM_KEYGEN_API_URL=https://keygen.titaniumlabs.us
TITANIUM_KEYGEN_ACCOUNT_ID=62ad28e5-2ce7-49d6-8170-ae6fa6584c86
TITANIUM_KEYGEN_PRODUCT_ID=469dcd2e-b41c-4fc9-ba34-1c5d444edb82
TITANIUM_KEYGEN_POLICY_ID=b2eb693f-e4f7-4d4a-b0aa-87fa45a18324

# Coolify-only (NEVER commit a real value here)
# Portal API token used by the migration script + admin tooling.
TITANIUM_KEYGEN_PORTAL_TOKEN= # Coolify only

# Optional in dev; STRONGLY recommended in prod (Redis-backed jti blocklist
# enforces magic-link single-use). When TITANIUM_REQUIRE_REDIS=true (default)
# and this is unset, the hub will hard-fail at boot.
TITANIUM_REDIS_URL=
TITANIUM_LICENSE_CACHE_TTL_SECONDS=300

# When true (default), missing TITANIUM_REDIS_URL is a fatal boot error so
# magic-link replay protection cannot silently degrade. Set to "false" ONLY
# for local dev without a Redis instance.
TITANIUM_REQUIRE_REDIS=true

# Optional shared secret for the Titanium → hub `license-changed` webhook.
# Unset = the webhook route returns 503 (`not_configured`) and is inert.
# Coolify only when configured.
TITANIUM_WEBHOOK_SECRET= # Coolify only (optional)

# ─── Session + magic-link secrets (Phase 07) ─────────────────────────────────
# Coolify only. Both must be ≥32 chars in prod. Rotate SESSION_SECRET ONLY if
# you intend to log every Titanium-cookie user out (D14 runbook rotates
# JWT_SECRET, NOT SESSION_SECRET — see docs/auth.md).
MAGIC_LINK_SECRET= # Coolify only, ≥32 chars
SESSION_SECRET= # Coolify only, ≥32 chars

# Dual-auth soak flag. true = legacy POST /api/auth/login + bearer JWT still
# work as fallback. false (post-cutover default) = only cookie+Titanium flow.
# Phase 07.5 removes the flag entirely.
ALLOW_LEGACY_LOGIN=false

# ─── Web (Vite, baked into the build) ────────────────────────────────────────
# Customer-facing portal URL the web UI links out to for "Manage account",
# "Change password", etc. Default https://license.titaniumlabs.us is used when
# unset, so this is only needed for non-default Titanium tenants.
VITE_TITANIUM_PORTAL_URL=
31 changes: 31 additions & 0 deletions .planning/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,34 @@ On first launch (no existing `supervisor.json`), the tray app SHALL open an onbo

### R-06-12 — Clean uninstall
Uninstalling via the bundled uninstaller SHALL remove: the app binaries, the autostart Run-key entry, and the tray icon. It SHALL prompt the user before deleting `%APPDATA%\remo-code\supervisor.json` and the `%LOCALAPPDATA%\remo-code-supervisor\audit.jsonl` — those are preserved by default.

---

## Phase 07 — titanium-auth-cutover

### R-AUTH-01 — Hub verifies Titanium EdDSA JWTs locally via JWKS
The hub SHALL verify every incoming Titanium-issued JWT locally using JWKS fetched from `${TITANIUM_KEYGEN_API_URL}/v1/accounts/${TITANIUM_ACCOUNT_ID}/.well-known/jwks.json`. Algorithm pinned to EdDSA (Ed25519) — RS*/ES*/HS* tokens claiming to be from Titanium SHALL be rejected. JWKS is cached in-memory with TTL ≥15 minutes; cache MISS on a presented `kid` triggers a single re-fetch (no stampede). Warm-cache fetch SHALL run during hub `bootstrap()` BEFORE the port is bound. Claims verified on every token: `iss == TITANIUM_KEYGEN_API_URL`, `aud` includes `TITANIUM_PRODUCT_ID`, `exp` valid, `nbf` valid, `iat` within ±30s skew, signature against the `kid`-matched key. No call to Titanium on the per-request hot path.

### R-AUTH-02 — Additive DB schema, zero data loss
Schema migration SHALL be additive only: add `users.titanium_user_id TEXT UNIQUE NULL`, drop NOT NULL from `users.password_hash` (column kept nullable). Existing rows SHALL retain their `password_hash` and bcrypt verification SHALL continue to work for any user during the soak. No column is dropped in this phase. `email UNIQUE` constraint preserved. Migration is idempotent (`ALTER TABLE … IF NOT EXISTS` patterns where Postgres supports it; gated migration otherwise).

### R-AUTH-03 — Idempotent mapping job, never auto-merges
A one-shot script `hub/scripts/map-users-to-titanium.ts` SHALL run before the dual-auth release. For each existing `users` row: (a) if no matching Keygen User by email exists, create one and write its UUID into `users.titanium_user_id`; (b) if a matching Keygen User exists AND was created by remo-code (verifiable via admin-API metadata or product scope), link by writing the UUID; (c) if a matching Keygen User exists AND was NOT created by remo-code (collision), `titanium_user_id` is left NULL and the row is appended to a `mapping_conflicts` log (table or file). The job SHALL support `--dry-run` (mandatory flag during first execution) and SHALL be safe to re-run (idempotent). Final output: counts of linked / created / conflicted.

### R-AUTH-04 — 2-week dual-auth soak window
Following the dual-auth release, the hub SHALL accept BOTH (a) Titanium EdDSA-signed JWTs and (b) legacy `JWT_SECRET`-signed HS256 JWTs for at least 14 days. Token type SHALL be detected by inspecting the `alg` header. The web SHALL default to Titanium magic-link login during the soak, with a visible "use password" fallback link routing to the legacy login form. Telemetry SHALL log per-login: token type, success/failure, latency. The soak ends only after ≥14 consecutive days with zero auth-related regressions.

### R-AUTH-05 — Post-soak cutover removes legacy login endpoint
After a successful ≥14d soak, the hub SHALL: (a) disable `/api/auth/login` (404 or 410), (b) remove the `JWT_SECRET`-signed user-token verify code path from REST + WS handlers, (c) remove the legacy login UI from the web (or hide behind `ALLOW_LEGACY_LOGIN` flag). Existing legacy JWTs in browsers SHALL continue to verify until natural expiry (≤7 days) during a brief overlap, OR clients SHALL be force-logged-out at cutover — planner picks based on traffic profile. `JWT_SECRET` env var stays if grep finds non-user-auth uses; otherwise removed.

### R-AUTH-06 — Revocation via Redis blocklist
The hub SHALL maintain a Redis blocklist (`titanium:blocklist` set with `titanium:blocklist:{subject_uuid}` keys) and check it on every token verify, in addition to the EdDSA signature check. A subject present in the blocklist SHALL be refused even if its JWT is technically unexpired. Redis client SHALL be in the hub process. Cache TTL for negative lookups (subject NOT blocked) is allowed to keep hot-path latency low; planner picks a safe default (≤60s).

### R-AUTH-07 — Rollback feature flag for ≥1 release post-cutover
A feature flag env var `ALLOW_LEGACY_LOGIN=true|false` SHALL be present and respected for at least one full release after cutover. When `true`, the bcrypt-verify code path and `/api/auth/login` endpoint are re-enabled. Default is `false` post-cutover. The bcrypt verify code SHALL stay present in the codebase (guarded by the flag) for the entire rollback window. `password_hash` column stays present (nullable) for the same window. Dropping either the flag, the bcrypt code, or the column happens in a follow-up phase — NOT this one.

### R-AUTH-08 — Stable identity by Titanium subject UUID, not email
The persistent identity key SHALL be the Titanium subject UUID (`sub` claim), stored in `users.titanium_user_id`. Email SHALL NOT be the identity key. On every authenticated request, the hub SHALL re-read `email` from the verified JWT claims; if it differs from the stored `users.email`, the hub SHALL update `users.email` keyed by `titanium_user_id`. `users.email UNIQUE` collision on update SHALL be logged and rejected (stale email kept until manual resolution). Brand-new Titanium logins (no matching remo-code row by `titanium_user_id` and no unlinked row matched by email) SHALL auto-create a `users` row keyed by the Titanium UUID with the default role.

### R-AUTH-09 — WS `/ws/client` accepts Titanium tokens with no protocol shape change
The `/ws/client` auth message SHALL accept the Titanium EdDSA JWT in the same shape it currently accepts the legacy JWT (`{ type: "auth", token }`). The verify branch SHALL inspect `alg` and route to either the JWKS-backed EdDSA verifier or the legacy HS256 verifier. Verify SHALL stay local (no Titanium round-trip per connection). Existing 30s heartbeat ping/pong, per-IP connection cap, and per-connection rate limits SHALL remain unchanged.
16 changes: 16 additions & 0 deletions .planning/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,19 @@ Source of truth for phase ordering, status, and dependencies. The GSD SDK parses
- `06-PLAN-005-protocol-enforcement` — wave 3 — Bun supervisor enforces security toggles in `process-manager.ts`: allowed-folders gate (with `realpath` symlink-escape check), git-only gate, max-concurrent cap, `--dangerously-skip-permissions` hard-strip when cap off, audit JSONL to `%LOCALAPPDATA%\remo-code-supervisor\audit.jsonl`, surface capability flags via `supervisor.hello`
- `06-PLAN-006-installer-and-autostart` — wave 3 — MSI installer via Tauri bundler; autostart Run-key on by default; coexist with NSSM (auto-detect existing service + offer one-click migrate to tray mode); uninstall removes autostart + binaries, prompts before deleting config/audit log
- `06-PLAN-007-docs-and-tests` — wave 4 — README + CLAUDE.md + new `docs/supervisor-tray.md`; Windows tray smoke checklist; Rust unit tests for IPC bridge pure logic; integration test for the sandbox-escape rejection

## Phase 07: titanium-auth-cutover

- Status: Pending
- Mode: standard
- Goal: Every remo-code user login goes through Titanium Licensing (Keygen CE-backed). Existing bcrypt users keep working with zero password resets. Hub verifies Titanium-issued EdDSA JWTs locally via JWKS + Redis blocklist; 2-week dual-auth soak; then legacy `/api/auth/login` and `JWT_SECRET`-signed user tokens removed. Agent `api_keys` stay local (out of scope).
- Depends on: [Phase 06]
- Requirements: [R-AUTH-01, R-AUTH-02, R-AUTH-03, R-AUTH-04, R-AUTH-05, R-AUTH-06, R-AUTH-07, R-AUTH-08, R-AUTH-09]
- Phase dir: `.planning/phases/07-titanium-auth-cutover/`
- Plans:
- `07-PLAN-001-schema-migration` — wave 1 — additive `users.titanium_user_id TEXT UNIQUE NULL`, drop NOT NULL from `users.password_hash`, optional `mapping_conflicts` table
- `07-PLAN-002-jwks-verify-and-blocklist` — wave 1 — `jose`-based EdDSA verifier with JWKS cache, Redis blocklist (`titanium:blocklist`) check, warm-cache on `bootstrap()` before port bind
- `07-PLAN-003-mapping-job` — wave 2 — idempotent one-shot `hub/scripts/map-users-to-titanium.ts` with `--dry-run`; create-if-not-exists in Titanium; never auto-merge collisions; conflict log
- `07-PLAN-004-dual-auth-middleware` — wave 2 — REST + WS auth handlers detect `alg` and branch between EdDSA (Titanium) verify and legacy HS256 (`JWT_SECRET`) verify; on-first-request linking by email for unlinked rows; email-sync-on-verify
- `07-PLAN-005-web-login-cutover` — wave 3 — web ships Titanium magic-link flow as the default; "use password" fallback link visible during soak; WS auth payload and REST headers attach Titanium token
- `07-PLAN-006-cutover-and-cleanup` — wave 4 — after ≥14d green soak: disable `/api/auth/login`, remove `JWT_SECRET` user-token verify code path (gated behind `ALLOW_LEGACY_LOGIN` flag for 1 release), update docs/README/CLAUDE.md
Loading
Loading