From 9327017da7e822d3a72a278db15ac6a2df2b249b Mon Sep 17 00:00:00 2001
From: Rokojori
+ Full design doc:
+ Roject's client is served as unbundled browser-native ESM — no bundler, Express
+ static serves individual
+ A one-shot Node.js script ( Rojo local-mode bug fixes: detection, creation, and system-prompt delivery (all caused by the same missing localRoot branching). Claude API provider: Rojo local-mode bug fixes (detection, creation, system-prompt). Claude API and Claude Code subprocess providers. Auth session refresh overhaul: 7 phases — rokojori-auth grace window, TokenUpdater, GuardedCall, Web Locks leader election, Electron token updater, new-session endpoint, heartbeat multi-instance login. library-ts: .js extensions added to 101 browser files for browser-native ESM compatibility.
User accounts are managed by rokojori-auth at
- Token refresh:
- rokojori-auth's page-level middleware (before
+ TokenUpdater (
+ GuardedCall (
Token extraction order (
Clock skew —
+ New-session endpoint (Auth session refresh overhaul — 7 phases
+ workspace/history/2026/08-August/02-Saturday/auth-update.page.
+ Root cause: two independent bugs — browser tabs racing a single-use refresh token,
+ and Electron never refreshing after its one-shot startup call.
+
+
+ refreshTokens
+ soft-deleted with usedAt/replacedBy; concurrent reuse
+ within 10 s resolves to the same replacement pair.TokenUpdater: central state machine
+ (valid|refreshing|expired|network-error) with 5-min timer +
+ ActivityAnalyser.onActive. jwtMiddleware proactively
+ rotates cookie server-side within 15 min of expiry.GuardedCall: singleton with three tiers
+ (user / editor / silent), pre-flight state check, retry with backoff.BroadcastChannel state sharing to followers.main.ts:
+ server clock offset via Date header, JWT exp decoded
+ directly, 5-min periodic proactive refresh.POST /api/auth/new-session in
+ rokojori-auth: mints a fresh independent token pair for an authenticated user.{ accessToken, timestamp } every 10 s; new instance skips login
+ if heartbeat ≤ 30 s old by calling /api/auth/new-session.library-ts: .js extensions added to all browser relative imports
+ .js files. All relative imports in
+ source/library-ts/browser/ were extension-less, causing cascading
+ 404s whenever any previously-unused module entered the import chain (first triggered
+ by TokenUpdater importing ActivityAnalyser).
+ fix-library-imports.js) added .js
+ to all relative imports across 101 files in a single pass. TypeScript with
+ moduleResolution: "bundler" accepts .js extensions in
+ source even for .ts files — clean build confirmed.
+ Saturday, 2 August 2026
- @anthropic-ai/sdk streaming, separate conversation history, tool use support. Claude Code subprocess provider: no API key needed, --print --output-format stream-json --verbose, --session-id/--resume for multi-turn. Settings panel: claude and claude-code endpoint types. Three bugs fixed: missing --verbose, stdin blocking, and shell: true mangling messages on Windows.account.rokojori.com — registration, login, JWT issuance, refresh
- token rotation, roles, and account deletion.
+ token rotation with a grace window, roles, and account deletion.
express.static)
- verifies the accessToken cookie but calls next() on
- any error — no redirect on expiry. Transparent refresh for API calls is handled
- by Roject's jwtMiddleware: when a TokenExpiredError
- hits an /api/ route and a refreshToken cookie is present,
- it calls POST AUTH_INTERNAL_HOST/api/auth/refresh server-side, sets
- the new cookies on the response, decodes the new JWT into req.user,
- and continues transparently. If refresh fails, the request falls through to
- requireAuth which returns 401. AUTH_INTERNAL_HOST
- defaults to AUTH_HOST; set it to http://localhost:3001
- in production to bypass nginx. The editor-shell checks
- GET /api/auth/me on startup and redirects to / on 401.
+ Refresh token grace window (rokojori-auth):
+ refreshTokens are soft-deleted — usedAt and
+ replacedBy are stamped on first use rather than the row being deleted.
+ A concurrent second refresh within REFRESH_GRACE_TTL (10 s) resolves
+ to the same replacement pair instead of 401ing. This is the baseline correctness
+ guarantee against races; client-side leader election is an optimisation on top.
+ source/auth/TokenUpdater.ts):
+ Central browser auth state machine. State enum: valid | refreshing | expired | network-error,
+ exposed via EventSlot. Triggers: a 5-minute periodic timer and
+ ActivityAnalyser.onActive (fires on mouse, touch, focus, and tab-visibility
+ restore). Each trigger pings GET /api/auth/me; jwtMiddleware
+ handles proactive cookie rotation server-side when within 15 min of expiry
+ (access token is httpOnly, so expiry is unreadable client-side).
+ Web Locks leader election: the first tab acquires roject-token-updater-leader
+ exclusively and broadcasts state to followers via BroadcastChannel.
+ Leader handoff is automatic when the holder tab closes.
+ source/auth/GuardedCall.ts):
+ Singleton wrapper for all API calls. Three tiers —
+ user: no retry, throws on failure;
+ editor: 3 retries at 1 s / 3 s / 8 s, then throws;
+ silent: same delays, never throws, console.warn on failure.
+ Pre-flight: blocks when state is expired; waits up to 6 s when
+ refreshing. Wired into Editor.ts (save = user, open = editor)
+ and editor-shell.ts (layout save/load = silent).
extractToken in auth.ts):
- Bearer header is checked before the accessToken cookie. This means an
- explicit Authorization: Bearer ... header always wins — critical for
- Electron (which injects tokens via onBeforeSendHeaders), API clients,
- and any context where a stale browser cookie might otherwise shadow a fresh token.
+ Bearer header is checked before the accessToken cookie. An explicit
+ Authorization: Bearer … header always wins — critical for Electron
+ (which injects tokens via onBeforeSendHeaders) and any context where a
+ stale cookie might shadow a fresh token.
JWT_CLOCK_TOLERANCE:
jwt.verify accepts a clockTolerance option (seconds).
- The env var JWT_CLOCK_TOLERANCE (default 0 / unset) is read as an
- integer and passed as clockTolerance when non-zero. Set to
- 7200 in .env for local development to absorb clock skew
- between the Windows dev machine and the production auth server. Never set this in
- production — if you need it there, fix the clock instead.
+ Set JWT_CLOCK_TOLERANCE=7200 in .env for local dev to
+ absorb skew between the Windows dev machine and the production auth server. Never
+ set this in production — fix the clock instead.
+ POST /api/auth/new-session):
+ requireAuth-guarded; mints a fresh independent token pair via
+ issueTokenPair for the authenticated user. Used by Electron to start
+ a new independent session from a running instance's heartbeat without re-login.
session.webRequest.onBeforeSendHeaders. Run with
npm run electron:dev.
+ Electron token updater: runs in electron/main.ts.
+ Reads the access token's exp directly from the JWT payload (token is
+ held in the main process, not behind an httpOnly cookie). Uses a
+ server clock offset derived from the Date response header of the first
+ auth-server call (_serverClockOffsetMs) for all expiry comparisons.
+ Refreshes proactively when within 15 min of expiry; checks every 5 min. Network
+ errors on refresh are silently retried next tick; auth failures (expired/revoked
+ refresh token) close the main window and show the login screen. Multiple Electron
+ instances each run their own updater with their own independent session — no
+ single-instance lock.
+
+ Heartbeat session sharing: a running instance writes
+ { accessToken, timestamp } to userData/session-heartbeat.json
+ every 10 s. A newly-starting instance reads it on launch; if ≤ 30 s old, it calls
+ POST /api/auth/new-session (requireAuth-guarded) to mint its own
+ independent token pair — skipping the login screen. If the heartbeat is stale or the
+ call fails, it falls through to normal login. Replaces the old plaintext
+ last-password.txt auto-login.
+
Local filesystem access: when ROJECT_ELECTRON=true,
the server mounts /api/local/ routes backed by Node.js fs
@@ -553,11 +593,22 @@
build/app/library-ts/browser/. The node part is included by
tsconfig.ts-node.json.
+ Import extension convention: all relative imports in
+ browser/ use explicit .js extensions
+ (e.g. from "../events/EventSlot.js"). Roject's client code is served
+ as unbundled browser-native ESM — no bundler resolves paths at build time, so the
+ browser fetches each module by its literal URL. Extension-less imports 404 because
+ Express static only serves the actual .js files. TypeScript with
+ moduleResolution: "bundler" accepts .js extensions in
+ source even when the source file is .ts.
+