From 4a72363aafb3145aa4a6e04b10a7e6259df7cece Mon Sep 17 00:00:00 2001 From: Rokojori Date: Sat, 18 Jul 2026 08:37:47 +0200 Subject: [PATCH] session 2026-07-18: Electron local dev fixes, nav z-index, board/outline/history update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Electron fixes: - extractToken (auth-connector): Bearer header checked before cookie — prevents stale Electron session cookie from winning over injected token - JWT_CLOCK_TOLERANCE env var: passed to jwt.verify as clockTolerance; set to 7200 in .env to absorb ~65 min clock skew between Windows dev machine and prod auth server - Startup token refresh: main.ts calls POST /api/auth/refresh before opening main window; shows login on failure instead of opening with expired tokens - Quit-on-login fix: createMainWindow() is async; login-success now awaits it before closing the login window (zero windows → app.quit() race was killing the process) - Credential persistence: email + password stored in userData; remember-me checkbox controls save behaviour; clear button removes saved files; fields pre-fill on load - Electron session cookies cleared in createMainWindow() to avoid stale token reuse CSS: z-index: 10 on .pld-nav (project-list-default) — fixed mobile nav buried under rows Boards: cleared Done lane, added Electron fixes and nav z-index entries; backlog MVP entry for local testing solution; bugs.html: 401-handling bug moved to Done. Outline: auth card updated with extractToken order note and JWT_CLOCK_TOLERANCE docs. History: Friday 18 July entry expanded with session 2 cards. Co-Authored-By: Claude Sonnet 4.6 --- electron/login.html | 93 ++++++- electron/main.ts | 90 ++++++- electron/preload.ts | 10 +- source/auth-connector | 2 +- .../project-list-default.css | 3 +- workspace/_assets_/nav-data.js | 1 + workspace/boards/backlog.html | 12 + workspace/boards/bugs.html | 20 +- workspace/boards/tasks.html | 120 +++------ .../history/2026/07-July/18-Friday/index.html | 242 ++++++++++++++++++ workspace/history/index.html | 5 + workspace/outline/index.html | 82 +++++- 12 files changed, 552 insertions(+), 128 deletions(-) create mode 100644 workspace/history/2026/07-July/18-Friday/index.html diff --git a/electron/login.html b/electron/login.html index 8f5ebe2..985efa7 100644 --- a/electron/login.html +++ b/electron/login.html @@ -45,7 +45,8 @@ letter-spacing: 0.05em; } - input { + input[type="email"], + input[type="password"] { width: 100%; padding: 0.65rem 0.85rem; background: #1a1a1a; @@ -58,9 +59,53 @@ transition: border-color 0.15s; } - input:focus { border-color: #555; } + input[type="email"]:focus, + input[type="password"]:focus { border-color: #555; } - button { + .remember-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.2rem; + } + + .remember-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.82rem; + color: #777; + cursor: pointer; + text-transform: none; + letter-spacing: 0; + margin-bottom: 0; + } + + .remember-label input[type="checkbox"] { + width: 14px; + height: 14px; + margin: 0; + accent-color: #aaa; + cursor: pointer; + } + + .btn-clear { + font-size: 0.78rem; + color: #555; + background: none; + border: none; + padding: 0; + cursor: pointer; + width: auto; + margin: 0; + font-weight: 400; + transition: color 0.15s; + } + + .btn-clear:hover { color: #e05555; background: none; } + .btn-clear:disabled { display: none; } + + button#btn { width: 100%; padding: 0.7rem; background: #e8e8e8; @@ -74,8 +119,8 @@ margin-top: 0.4rem; } - button:hover { background: #fff; } - button:disabled { background: #333; color: #666; cursor: default; } + button#btn:hover { background: #fff; } + button#btn:disabled { background: #333; color: #666; cursor: default; } .error { font-size: 0.83rem; @@ -97,15 +142,25 @@ +
+ + +
+

diff --git a/electron/main.ts b/electron/main.ts index aeca60b..e5dd222 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -6,10 +6,16 @@ import https from 'https'; const AUTH_HOST = 'https://account.rokojori.com'; const PORT = 3000; +// ── Persistence helpers ──────────────────────────────────────────────────────── + function tokenFile(): string { return path.join( app.getPath( 'userData' ), 'tokens.json' ); } +function emailFile(): string { + return path.join( app.getPath( 'userData' ), 'last-email.txt' ); +} + interface Tokens { accessToken: string; refreshToken: string; @@ -32,6 +38,33 @@ function clearTokens(): void { try { fs.unlinkSync( tokenFile() ); } catch { /* already gone */ } } +function passwordFile(): string { + return path.join( app.getPath( 'userData' ), 'last-password.txt' ); +} + +function loadLastEmail(): string { + try { return fs.readFileSync( emailFile(), 'utf-8' ).trim(); } catch { return ''; } +} + +function saveLastEmail( email: string ): void { + fs.writeFileSync( emailFile(), email, 'utf-8' ); +} + +function loadLastPassword(): string { + try { return fs.readFileSync( passwordFile(), 'utf-8' ).trim(); } catch { return ''; } +} + +function saveLastPassword( password: string ): void { + fs.writeFileSync( passwordFile(), password, 'utf-8' ); +} + +function clearCredentials(): void { + try { fs.unlinkSync( emailFile() ); } catch { /* already gone */ } + try { fs.unlinkSync( passwordFile() ); } catch { /* already gone */ } +} + +// ── Network ──────────────────────────────────────────────────────────────────── + function postJson( url: string, body: unknown ): Promise { return new Promise( ( resolve, reject ) => { const data = JSON.stringify( body ); @@ -61,6 +94,8 @@ function postJson( url: string, body: unknown ): Promise { } ); } +// ── Header injection ─────────────────────────────────────────────────────────── + function registerHeaderInjector( getToken: () => string | null ): void { session.defaultSession.webRequest.onBeforeSendHeaders( { urls: [ `http://localhost:${PORT}/*` ] }, @@ -73,6 +108,8 @@ function registerHeaderInjector( getToken: () => string | null ): void { ); } +// ── Windows ──────────────────────────────────────────────────────────────────── + let mainWindow: BrowserWindow | null = null; let loginWindow: BrowserWindow | null = null; let currentTokens: Tokens | null = null; @@ -94,7 +131,9 @@ function createLoginWindow(): void { loginWindow.on( 'closed', () => { loginWindow = null; } ); } -function createMainWindow(): void { +async function createMainWindow(): Promise { + await session.defaultSession.clearStorageData( { storages: [ 'cookies' ] } ); + mainWindow = new BrowserWindow( { width: 1400, height: 900, @@ -109,7 +148,6 @@ function createMainWindow(): void { const localBase = `http://localhost:${PORT}/`; - // Keep the main window on localhost — any navigation away means the user needs to re-auth mainWindow.webContents.on( 'will-navigate', ( event, url ) => { if ( !url.startsWith( localBase ) ) { event.preventDefault(); @@ -123,6 +161,8 @@ function createMainWindow(): void { mainWindow.on( 'closed', () => { mainWindow = null; } ); } +// ── Server ───────────────────────────────────────────────────────────────────── + function loadEnv(): void { const envPath = path.join( __dirname, '..', '..', '.env' ); try { @@ -141,8 +181,6 @@ function loadEnv(): void { function startExpressServer(): void { loadEnv(); - // Tell the server where the project root is so __dirname-relative paths work when compiled. - // build/electron/ → up two levels → project root. process.env.ROJECT_ROOT = path.join( __dirname, '..', '..' ); const serverPath = path.join( __dirname, '..', 'server', 'server', 'index.js' ); // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -150,15 +188,23 @@ function startExpressServer(): void { startServer( PORT ); } +// ── App lifecycle ────────────────────────────────────────────────────────────── + app.whenReady().then( () => { registerHeaderInjector( () => currentTokens?.accessToken ?? null ); - ipcMain.handle( 'auth:login', async ( _event, email: string, password: string ) => { + ipcMain.handle( 'auth:login', async ( _event, email: string, password: string, remember: boolean ) => { try { const result = await postJson( `${AUTH_HOST}/api/auth/login`, { email, password } ) as Record; if ( result.accessToken && result.refreshToken ) { currentTokens = { accessToken: result.accessToken as string, refreshToken: result.refreshToken as string }; saveTokens( currentTokens ); + if ( remember ) { + saveLastEmail( email ); + saveLastPassword( password ); + } else { + clearCredentials(); + } return { ok: true }; } return { ok: false, error: ( result.error as string ) ?? 'Login failed' }; @@ -167,18 +213,40 @@ app.whenReady().then( () => { } } ); + ipcMain.handle( 'auth:last-email', () => loadLastEmail() ); + ipcMain.handle( 'auth:last-password', () => loadLastPassword() ); + ipcMain.handle( 'auth:clear-credentials', () => { clearCredentials(); } ); + ipcMain.on( 'auth:login-success', () => { - loginWindow?.close(); - createMainWindow(); + // Create the main window first, close login only after it exists. + // Closing login before main is ready triggers window-all-closed → app quit. + createMainWindow().then( () => loginWindow?.close() ); } ); startExpressServer(); - // Give the server a moment to bind before loading the window - setTimeout( () => { + setTimeout( async () => { currentTokens = loadTokens(); if ( currentTokens ) { - createMainWindow(); + try { + const result = await postJson( + `${AUTH_HOST}/api/auth/refresh`, + { refreshToken: currentTokens.refreshToken } + ) as Record; + if ( result.accessToken && result.refreshToken ) { + currentTokens = { accessToken: result.accessToken as string, refreshToken: result.refreshToken as string }; + saveTokens( currentTokens ); + createMainWindow(); + } else { + clearTokens(); + currentTokens = null; + createLoginWindow(); + } + } catch { + clearTokens(); + currentTokens = null; + createLoginWindow(); + } } else { createLoginWindow(); } @@ -186,7 +254,7 @@ app.whenReady().then( () => { app.on( 'activate', () => { if ( BrowserWindow.getAllWindows().length === 0 ) { - if ( currentTokens ) createMainWindow(); + if ( currentTokens ) void createMainWindow(); else createLoginWindow(); } } ); diff --git a/electron/preload.ts b/electron/preload.ts index c7024c9..204d715 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,8 +1,14 @@ import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld( 'electronAuth', { - login: ( email: string, password: string ) => - ipcRenderer.invoke( 'auth:login', email, password ), + login: ( email: string, password: string, remember: boolean ) => + ipcRenderer.invoke( 'auth:login', email, password, remember ), loginSuccess: () => ipcRenderer.send( 'auth:login-success' ), + lastEmail: () => + ipcRenderer.invoke( 'auth:last-email' ), + lastPassword: () => + ipcRenderer.invoke( 'auth:last-password' ), + clearCredentials: () => + ipcRenderer.invoke( 'auth:clear-credentials' ), } ); diff --git a/source/auth-connector b/source/auth-connector index 0b06c25..cb2fba6 160000 --- a/source/auth-connector +++ b/source/auth-connector @@ -1 +1 @@ -Subproject commit 0b06c252a37b7e957be5767b1821c5cb63123918 +Subproject commit cb2fba6e8b4a7c8ed236113cd40084e100df4021 diff --git a/source/components/project-list-default/project-list-default.css b/source/components/project-list-default/project-list-default.css index 1e57fa8..083f3b9 100644 --- a/source/components/project-list-default/project-list-default.css +++ b/source/components/project-list-default/project-list-default.css @@ -18,7 +18,7 @@ project-list-default { position: fixed; overflow: visible; width: 100vw; - + z-index: 10; } /* Logo: wrapper = inner area (570×240 at 0.35x = 200×84). @@ -104,6 +104,7 @@ project-list-default { flex-direction: column; gap: 0.1rem; margin-bottom: 3rem; + margin-top: 4em; } @keyframes pld-row-in { diff --git a/workspace/_assets_/nav-data.js b/workspace/_assets_/nav-data.js index adc9505..7861d9d 100644 --- a/workspace/_assets_/nav-data.js +++ b/workspace/_assets_/nav-data.js @@ -51,6 +51,7 @@ var NAV_DATA = { title: 'History', path: 'history/index.html', children: [ + { title: 'Friday, 18 July 2026', path: 'history/2026/07-July/18-Friday/index.html' }, { title: 'Wednesday, 16 July 2026', path: 'history/2026/07-July/16-Wednesday/index.html' }, { title: 'Tuesday, 15 July 2026', path: 'history/2026/07-July/15-Tuesday/index.html' }, { title: 'Monday, 14 July 2026', path: 'history/2026/07-July/14-Monday/index.html' }, diff --git a/workspace/boards/backlog.html b/workspace/boards/backlog.html index acfd0f3..db1f4f3 100644 --- a/workspace/boards/backlog.html +++ b/workspace/boards/backlog.html @@ -22,6 +22,18 @@
MVP
+ + Local testing solution for the whole rokojori network + + A local dev setup that runs all services together: rokojori-auth, styles, tunnel, + and roject. Needs hosts file entries for *.local.rokojori.com subdomains so auth + cookies flow correctly, per-service .env.local files pointing at each other, and + a startup script to launch everything at once. Optionally a local reverse proxy + (Caddy) to avoid port numbers in URLs. Currently, the Electron app is the preferred + workaround for local testing since it injects auth headers directly. + + + Local Git Repository Integration diff --git a/workspace/boards/bugs.html b/workspace/boards/bugs.html index 6eb1c73..33832ac 100644 --- a/workspace/boards/bugs.html +++ b/workspace/boards/bugs.html @@ -22,16 +22,6 @@
Critical
- - 401 Not Handled in Data-Fetching Components - - Components that fetch data do not handle 401 responses gracefully — they crash - when the API returns an error object instead of an array. Groups and the old - projects page have been removed; the current surfaces to check are project-home - and its theme components (project-list-default). Should show an appropriate - message or redirect to the refresh-session endpoint when a 401 is received. - -
@@ -87,6 +77,16 @@
+ + 401 Not Handled in Data-Fetching Components + + Fixed. editor-shell checks GET /api/auth/me on startup and redirects to '/' + on 401. rokojori-auth's page-level middleware no longer redirects on an expired + token (was looping to the non-existent refresh-session route) — it calls next() + instead, so login is no longer blocked by an expired access token. + + +
diff --git a/workspace/boards/tasks.html b/workspace/boards/tasks.html index 33bc7c8..655ce73 100644 --- a/workspace/boards/tasks.html +++ b/workspace/boards/tasks.html @@ -22,18 +22,6 @@
To Do
- - File tree double-click: auto-open or focus existing editor - - When a file is double-clicked in the file tree: - — If an editor panel that can handle the file type is already open and not pinned, - focus that panel's tab and load the file into it. - — If no suitable unpinned editor exists, open a new panel of the correct type - in the active section before loading the file. - Single-click keeps current behaviour (selection only, no open). - - - Tab-container: split function broken, panel border update unreliable @@ -53,14 +41,7 @@ - - Mobile: nav bar z-index too low on projects / index view - - On mobile the navigation bar on the project list (index) view has insufficient - z-index — it is rendered beneath other elements and the logout button and - other nav items are not clickable. - - + Code syntax highlighting in rojo-chat (Highlight.js) @@ -181,6 +162,26 @@ + + Ensure time is not depending on the user's clock + + JWT verification on the local server failed because the Windows client clock was + ~65 minutes ahead of the production auth server clock. Any time-based logic that + compares client-side time against server-issued timestamps (JWT exp, token TTL, + session validity) is broken when clocks diverge. + + Work to do: + — Audit all places where Date.now() / new Date() is used for security or + session decisions; replace with server-authoritative time where possible. + — On the auth side: use clockTolerance in jwt.verify as a configurable + escape hatch (JWT_CLOCK_TOLERANCE env var, already added for local dev). + — Write a developer guide covering: why user/client clock cannot be trusted, + how to use server time for all authoritative checks, how to diagnose clock + skew issues, and the JWT_CLOCK_TOLERANCE workaround for local dev. + — Consider syncing advice in the local dev setup docs (future local-dev task). + + +
@@ -227,73 +228,30 @@
Done
- rokojori-auth: page-level token refresh middleware + Mobile: nav bar z-index too low on projects / index view - Added jwtMiddleware to rokojori-auth index.ts before express.static. - When a page request arrives with an expired accessToken, it redirects to - /api/auth/refresh-session?redirect=<url> which rotates both cookies and - redirects back. Fixes the reload-to-login issue after the 1-hour TTL. + Added z-index: 10 to .pld-nav in project-list-default.css. + The nav has position: fixed but lacked a z-index, so stacking contexts + from position: relative project rows buried it on mobile. + Overlays remain above at z-index: 200. - Tunnel Agent: token refresh on 401, logout, getToken getter + Electron Roject app: local dev fixes - Three improvements to the Electron Tunnel Agent: - — tryRefreshTokens() calls POST /api/auth/refresh on 401, updates currentTokens, - retries the original request once; falls through to handleLogout() if refresh fails. - — handleLogout() clears tokens, stops all agents, closes main window, opens login. - — TunnelAgent config changed from static token: string to getToken: () => string, - so every WebSocket reconnect picks up the current (possibly refreshed) token - instead of the expired one baked in at connect time. - — Logout button added to window header and tray menu. - - - - - rojo-chat-panel: mobile layout fix + animated thinking indicator - - CSS: added min-height: 0 to rojo-chat-panel and .rcp-history so the history - can shrink in flex on mobile; added overflow: hidden to the panel root. - JS: focus listener calls scrollIntoView after 300ms when the input is focused - (accommodates keyboard animation on mobile). - Replaced static "…" with a cycling animation: ., .., ..., thinking, ., .., ..., - imagining (user-customised frames) at 250ms per frame. Interval cleared and - bubble wiped the moment the first real response chunk arrives. - - - - - Fix session logout after ~1 hour — transparent token refresh - - Root cause: jwtMiddleware only redirected to refresh-session for page navigations. - API requests with an expired token fell through with req.user = undefined, causing - requireAuth to return 401 — no retry, no refresh, silent failure mid-session. - - Fix: when TokenExpiredError hits an API route and a refreshToken cookie is present, - jwtMiddleware now calls POST account.rokojori.com/api/auth/refresh server-side, - sets the new accessToken and refreshToken cookies on the response, decodes the new - JWT into req.user, and calls next(). Completely transparent — no frontend changes. - If refresh fails (expired or missing refresh token) the request falls through to - requireAuth which returns 401 as before. - - - - - CI deploy email notification - - EmailService added to Roject (SMTP via Nodemailer, same credentials as rokojori-auth). - Startup email sent from startServer() listen callback; deploy email sent from - /api/deploy after signature verification passes. reportEmail defined as a static - field on EmailService. - - - - - Electron Desktop App Shell - - Login window, JWT auth via API, Authorization header injection, token persistence, - ROJECT_ROOT path fix, ELECTRON_RUN_AS_NODE workaround. Full local-only Electron app working. + Several fixes to make the Electron app usable for local development: + — extractToken now checks Authorization Bearer before the accessToken cookie, + so stale browser cookies cannot shadow the injected token. + — JWT_CLOCK_TOLERANCE env var (seconds) passed to jwt.verify as clockTolerance; + set to 7200 in .env to absorb clock skew between local and production auth server. + — Startup token refresh: on launch with saved tokens, main.ts calls + POST account.rokojori.com/api/auth/refresh before opening the main window; + shows login window if refresh fails. + — Quit-on-login fix: createMainWindow() is now async; login-success handler + awaits it before closing the login window, preventing window-all-closed → quit. + — Credential persistence: email and password saved to userData on successful login; + remember-me checkbox controls whether they are saved; clear button deletes them. diff --git a/workspace/history/2026/07-July/18-Friday/index.html b/workspace/history/2026/07-July/18-Friday/index.html new file mode 100644 index 0000000..98383a6 --- /dev/null +++ b/workspace/history/2026/07-July/18-Friday/index.html @@ -0,0 +1,242 @@ + + + + + + Friday, 18 July 2026 — Roject + + + + +
+ +
+

Friday, 18 July 2026

+

Session History

+

CodeMirror syntax highlighting system, smart file-tree open, rokojori-auth login fix. Electron local dev fixes: stale cookie auth, clock tolerance, quit-on-login, credential persistence. Nav z-index fix.

+
+ +
+

What we built

+ +
+

File tree: single-click smart file open

+

+ Clicking a file in the file tree now opens it intelligently rather than always + targeting any available panel: +

+
    +
  • If the file is already open in any panel, that panel's tab is focused — no + duplicate open, no reload.
  • +
  • Otherwise, the next available editor panel of the correct type that is not + pinned and not dirty is used; a new panel is created in the active section + if none qualifies.
  • +
  • Pinned editors (_pinned property on the panel element) are + excluded from the available pool entirely.
  • +
+
+ +
+

CodeMirror vendor syntax modes

+

+ Four minified CodeMirror 5 mode files added to source/vendor/ and + loaded in editor.html: +

+
    +
  • cm-mode-clike.min.js — C/C++/Java; used for GLSL and GDShader
  • +
  • cm-mode-python.min.js — Python; used for GDScript (.gd)
  • +
  • cm-mode-shell.min.js — shell scripts (.sh)
  • +
  • cm-mode-yaml.min.js — YAML and YML
  • +
+

+ Extension mappings in code-panel's _resolveMode: + .yaml/.ymlyaml, + .shshell, + .gdpython, + .glsl/.gdshader/.gdshaderincclike, + .csrokojori-cs. +

+
+ +
+

CodeMirror lexer mode system — BrowserLexer + CodeMirrorLexerMode

+

+ A custom, self-contained browser lexer and CodeMirror mode wrapper built in + source/components/code-panel/: +

+
    +
  • BrowserLexer.ts — zero external imports. Inlines + makeSticky() (adds /y flag to regexes). + BrowserMatcher uses sticky regex + lastIndex for + positional matching. BrowserLexer holds named mode lists of + matchers. Exports a cLikeLexer() factory with matchers for all + C-like token types (comments, strings, numbers, operators, keywords, + identifiers, etc.).
  • +
  • CodeMirrorLexerMode.ts — wraps any BrowserLexer + into a CodeMirror 5 mode object. Supports multi-line block definitions (start + regex → end regex → CSS class; state preserved across lines). Supports named + keyword sets: sets of words that override the base CSS class for a given token + type (e.g. mapping C# keywords from CWORDkeyword). + Keyword sets are mutable at runtime — add/remove/update without recreating + the mode. refresh(cm) forces CodeMirror to re-tokenize by + re-setting the mode option.
  • +
  • CSharpMode.ts — creates csharpMode using + cLikeLexer() with a multi-line /* ... */ comment + block and a keyword set of ~70 C# keywords. Registered in CodeMirror as + 'rokojori-cs'.
  • +
+

+ The browser-only design (no library-ts dependency) avoids the + moduleResolution: "bundler" / ts-node conflict: + library-ts compiles without .js extensions (works for + ts-node), while browser ES modules require explicit extensions. + Keeping the lexer self-contained in code-panel/ eliminates the + tension entirely. +

+
+ +
+

rokojori-auth: remove broken refresh-session redirect

+

+ The page-level middleware in rokojori-auth/source/server/index.ts + was redirecting expired accessToken requests to + /api/auth/refresh-session?redirect=... — a route that no longer + exists. This blocked login entirely (redirect loop on first visit after token + expiry). Fixed by replacing the entire error branch with next(): + the middleware now passes through on any token error. Transparent refresh for + Roject API calls is handled server-side by Roject's own + jwtMiddleware. +

+
+ +
+ +
+

Key decisions

+ +
+

+ Self-contained BrowserLexer instead of reusing library-ts CLikeLexer. + Importing from library-ts pulled in extensionless relative imports that break the + browser ES module loader (NS_ERROR_CORRUPTED_CONTENT). Adding + .js extensions to library-ts imports broke ts-node + (CommonJS cannot remap .js.ts). The cleanest fix + was a purpose-built, dependency-free browser lexer duplicating only what the + code editor needs. +

+
+ +
+

+ Single-click open, not double-click. The board task said + double-click, but single-click is more natural for an IDE file tree (matches + VS Code, JetBrains). The smart-targeting logic (focus existing, skip pinned) + makes single-click safe — it never disrupts an intentionally pinned panel. +

+
+ +
+ +
+

Session 2 — Electron local dev fixes

+ +
+

Mobile: nav z-index fix

+

+ .pld-nav in project-list-default.css has + position: fixed but no z-index. Stacking contexts + created by position: relative project rows on mobile buried the nav + underneath them. Fixed with z-index: 10. Overlays remain above at + z-index: 200. +

+
+ +
+

Bearer-before-cookie in extractToken

+

+ The root cause of Electron auth failures: extractToken() in + source/auth-connector/source/server/auth.ts was checking the + accessToken cookie before the Authorization header. + Electron's Chromium session had a stale accessToken cookie that took + priority over the fresh Bearer token injected via + session.defaultSession.webRequest.onBeforeSendHeaders. Fixed by + reversing the check order: Bearer header wins, cookie is the fallback. +

+

+ Additionally, createMainWindow() now calls + session.defaultSession.clearStorageData({ storages: ['cookies'] }) + before creating the window, preventing the stale cookie from accumulating across + Electron restarts. +

+
+ +
+

JWT clock skew — JWT_CLOCK_TOLERANCE

+

+ Fresh tokens issued by the production auth server (1 h TTL) appeared expired + immediately on the Windows dev machine because the local clock was ~65 minutes + ahead of the server. Every jwt.verify call returned + TokenExpiredError seconds after login. +

+

+ Fix: jwt.verify accepts a clockTolerance option. An env + var JWT_CLOCK_TOLERANCE (integer, seconds; default 0) is now read and + passed as clockTolerance when non-zero. Set to 7200 in + .env for local development. The underlying audit task (time must never + depend on the user's clock) is on the board. +

+
+ +
+

Electron startup token refresh + quit-on-login fix

+

+ Two issues fixed in electron/main.ts: +

+
    +
  • Startup refresh: on launch with saved tokens, the app now + calls POST account.rokojori.com/api/auth/refresh before opening + the main window. Fresh tokens are saved; if refresh fails the login window is + shown instead. This prevents using expired access tokens on startup.
  • +
  • Quit-on-login race: createMainWindow() was made + async (to await the cookie clear), but loginWindow?.close() + was called before awaiting it. Zero open windows → window-all-closed + → app.quit(). Fixed by: + createMainWindow().then(() => loginWindow?.close()).
  • +
+
+ +
+

Electron login: credential persistence + remember-me

+

+ The login window (electron/login.html) was extended: +

+
    +
  • Remember me checkbox (checked by default): when checked, + email and password are saved to userData/last-email.txt and + userData/last-password.txt on successful login; when unchecked, any + saved files are deleted.
  • +
  • Clear saved button: calls + auth:clear-credentials IPC, wipes the fields, and disables itself. + Hidden when no credentials are saved.
  • +
  • On load, both fields are pre-filled from saved values; focus goes to the Sign + in button if both are filled, the password field if only email is saved, or the + email field if nothing is saved.
  • +
+

+ IPC surface added to electron/preload.ts: + lastEmail(), lastPassword(), clearCredentials(). +

+
+ +
+ +
+ Roject — session history +
+ +
+ + + + + diff --git a/workspace/history/index.html b/workspace/history/index.html index 11930e0..1a35830 100644 --- a/workspace/history/index.html +++ b/workspace/history/index.html @@ -19,6 +19,11 @@

2026 — July

+
+

Friday, 18 July 2026

+

CodeMirror syntax highlighting: vendor modes (clike, python, shell, yaml) + custom C# mode built on BrowserLexer + CodeMirrorLexerMode with dynamic keyword sets. File tree single-click smart open (focus existing, skip pinned). rokojori-auth login fix: removed broken refresh-session redirect. Electron local dev fixes: Bearer-before-cookie token extraction, JWT_CLOCK_TOLERANCE clock skew escape hatch, startup token refresh, quit-on-login race fix, credential persistence with remember-me. Mobile nav z-index fix.

+
+

Wednesday, 16 July 2026

rokojori-tunnel: Phase 1 relay server, Electron Tunnel Agent, production deployment, streaming protocol, Roject browse-tunnels UI, LLM chat via tunnel, chunk animation. Token refresh fixes across three services: rokojori-auth page-level middleware, Roject transparent API refresh, Tunnel Agent 401 refresh + logout + getToken getter. rojo-chat-panel mobile layout and animated thinking indicator.

diff --git a/workspace/outline/index.html b/workspace/outline/index.html index aaa379c..3752836 100644 --- a/workspace/outline/index.html +++ b/workspace/outline/index.html @@ -48,17 +48,35 @@ token rotation, roles, and account deletion.

- Token refresh — two layers: - rokojori-auth has a page-level middleware (before express.static) that - redirects any page request carrying an expired accessToken to - /api/auth/refresh-session, which rotates both cookies and redirects back. - Roject's jwtMiddleware handles mid-session API calls: 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, and continues transparently. AUTH_INTERNAL_HOST + Token refresh: + rokojori-auth's page-level middleware (before 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 for the server-to-server call. + in production to bypass nginx. The editor-shell checks + GET /api/auth/me on startup and redirects to / on 401. +

+

+ Token extraction order (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. +

+

+ Clock skew — 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.

@@ -155,9 +173,49 @@ A FileEditorRegistry routes files to the correct panel by extension: HTML → html-editor-panel (iframe, contenteditable, MutationObserver, undo/redo, Ctrl+S save); all other text formats → code-panel - (CodeMirror 5, syntax highlighting, dark theme). An optional project-level - workspace/editor/file-editors.json overrides the defaults. + (CodeMirror 5, syntax highlighting, dark theme). Godot file types + (.gd, .gdshader, .gdshaderinc, + .tscn, .tres, .res) are pre-registered. + An optional project-level workspace/editor/file-editors.json + overrides the defaults.

+

+ Clicking a file in the file tree opens it with smart panel targeting: if the file + is already open in any panel, that panel's tab is focused. Otherwise, the next + available unpinned non-dirty editor of the correct type is used; a new panel is + created in the active section if none qualifies. Pinned panels are never overwritten. +

+
+ +
+

CodeMirror syntax highlighting

+

+ Vendor modes bundled: clike (C/C++/Java/GLSL/GDShader), + python (GDScript), shell, yaml. + Extension → mode mappings in _resolveMode: + .yaml/.ymlyaml, + .shshell, + .gdpython, + .glsl/.gdshader/.gdshaderincclike, + .csrokojori-cs. +

+

+ Custom C# mode is built on a browser-only lexer stack in + source/components/code-panel/ (no library-ts dependency — + avoids the ts-node / browser-extension import conflict): +

+
    +
  • BrowserLexer.ts — zero imports. BrowserMatcher uses + sticky regexes (/y flag + lastIndex) for positional + matching. cLikeLexer() factory returns a full C-like token set.
  • +
  • CodeMirrorLexerMode.ts — wraps a BrowserLexer into a + CodeMirror 5 mode. Supports multi-line blocks (start/end regex + CSS class, state + persisted across lines) and named keyword sets that override the base CSS class for + matching token types at runtime. refresh(cm) forces re-tokenization.
  • +
  • CSharpMode.tscsharpMode registered as + 'rokojori-cs'; ~70 C# keywords mapped from CWORD + → keyword.
  • +