Compare commits

..

No commits in common. "9327017da7e822d3a72a278db15ac6a2df2b249b" and "6f4d5c9ff201a47d8e63eb14b553b91fa2b3b49a" have entirely different histories.

5 changed files with 26 additions and 144 deletions

@ -1 +1 @@
Subproject commit 3884dd4edb5dbaad8d4c5cb95837ee196dd8080b
Subproject commit 7398efdfad12caec1ddf71968992543b0b936e76

View File

@ -22,24 +22,6 @@
<div class="lane">
<div class="lane-header">To Do</div>
<task-item class="blue hide-content">
<task-title>rokojori-auth: end-to-end testing after session refresh overhaul</task-title>
<task-content>
The 7-phase auth session refresh overhaul (grace window, TokenUpdater, GuardedCall,
Web Locks leader election, Electron token updater, new-session endpoint, heartbeat
login) is deployed but not yet fully tested end-to-end. Verify:
— Browser: multiple tabs open, let access token near-expire; confirm only one tab
refreshes and others get state via BroadcastChannel.
— Browser: confirm GuardedCall retries on transient network errors and blocks on expired.
— Electron: let app run &gt;1h, confirm token is refreshed proactively without restart.
— Electron multi-instance: open two projects, confirm second instance skips login via
heartbeat and mints an independent session.
— rokojori-auth grace window: force a near-simultaneous double-refresh and confirm
the second call resolves to the same replacement pair instead of 401ing.
</task-content>
</task-item>
<task-item class="blue hide-content">
<task-title>Rojo Chat: LLM Tools</task-title>
<task-content>

View File

@ -13,7 +13,7 @@
<header>
<p class="date">Saturday, 2 August 2026</p>
<h1>Session History</h1>
<p class="subtitle">Rojo bug fixes, Claude API and Claude Code providers, auth session refresh overhaul (7 phases), library-ts ESM import fix.</p>
<p class="subtitle">Rojo bug fixes (local mode detection, creation, system prompt). Claude API provider and Claude Code subprocess provider for rojo-chat-panel.</p>
</header>
<section>
@ -118,55 +118,6 @@
</p>
</div>
</section>
<div class="card">
<h3>Auth session refresh overhaul — 7 phases</h3>
<p>
Full design doc: <code>workspace/history/2026/08-August/02-Saturday/auth-update.page</code>.
Root cause: two independent bugs — browser tabs racing a single-use refresh token,
and Electron never refreshing after its one-shot startup call.
</p>
<ul style="margin-top:0.5rem;line-height:1.9">
<li><strong>Phase 1</strong> — rokojori-auth grace window: <code>refreshTokens</code>
soft-deleted with <code>usedAt</code>/<code>replacedBy</code>; concurrent reuse
within 10 s resolves to the same replacement pair.</li>
<li><strong>Phase 2</strong><code>TokenUpdater</code>: central state machine
(<code>valid|refreshing|expired|network-error</code>) with 5-min timer +
<code>ActivityAnalyser.onActive</code>. <code>jwtMiddleware</code> proactively
rotates cookie server-side within 15 min of expiry.</li>
<li><strong>Phase 3</strong><code>GuardedCall</code>: singleton with three tiers
(user / editor / silent), pre-flight state check, retry with backoff.</li>
<li><strong>Phase 4</strong> — Web Locks leader election across browser tabs;
<code>BroadcastChannel</code> state sharing to followers.</li>
<li><strong>Phase 5</strong> — Electron token updater in <code>main.ts</code>:
server clock offset via <code>Date</code> header, JWT <code>exp</code> decoded
directly, 5-min periodic proactive refresh.</li>
<li><strong>Phase 6</strong><code>POST /api/auth/new-session</code> in
rokojori-auth: mints a fresh independent token pair for an authenticated user.</li>
<li><strong>Phase 7</strong> — Electron heartbeat: running instance writes
<code>{ accessToken, timestamp }</code> every 10 s; new instance skips login
if heartbeat ≤ 30 s old by calling <code>/api/auth/new-session</code>.</li>
</ul>
</div>
<div class="card">
<h3>library-ts: .js extensions added to all browser relative imports</h3>
<p>
Roject's client is served as unbundled browser-native ESM — no bundler, Express
static serves individual <code>.js</code> files. All relative imports in
<code>source/library-ts/browser/</code> were extension-less, causing cascading
404s whenever any previously-unused module entered the import chain (first triggered
by <code>TokenUpdater</code> importing <code>ActivityAnalyser</code>).
</p>
<p style="margin-top:0.75rem">
A one-shot Node.js script (<code>fix-library-imports.js</code>) added <code>.js</code>
to all relative imports across 101 files in a single pass. TypeScript with
<code>moduleResolution: "bundler"</code> accepts <code>.js</code> extensions in
source even for <code>.ts</code> files — clean build confirmed.
</p>
</div>
</section>
<section>

View File

@ -21,7 +21,7 @@
<div class="card">
<h3><a href="2026/08-August/02-Saturday/index.html">Saturday, 2 August 2026</a></h3>
<p>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.</p>
<p>Rojo local-mode bug fixes: detection, creation, and system-prompt delivery (all caused by the same missing localRoot branching). Claude API provider: <code>@anthropic-ai/sdk</code> streaming, separate conversation history, tool use support. Claude Code subprocess provider: no API key needed, <code>--print --output-format stream-json --verbose</code>, <code>--session-id</code>/<code>--resume</code> for multi-turn. Settings panel: <code>claude</code> and <code>claude-code</code> endpoint types. Three bugs fixed: missing <code>--verbose</code>, stdin blocking, and <code>shell: true</code> mangling messages on Windows.</p>
</div>
</section>

View File

@ -45,57 +45,38 @@
<p>
User accounts are managed by <strong>rokojori-auth</strong> at
<code>account.rokojori.com</code> — registration, login, JWT issuance, refresh
token rotation with a grace window, roles, and account deletion.
token rotation, roles, and account deletion.
</p>
<p style="margin-top:0.75rem">
<strong>Refresh token grace window (rokojori-auth):</strong>
<code>refreshTokens</code> are soft-deleted — <code>usedAt</code> and
<code>replacedBy</code> are stamped on first use rather than the row being deleted.
A concurrent second refresh within <code>REFRESH_GRACE_TTL</code> (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.
</p>
<p style="margin-top:0.75rem">
<strong>TokenUpdater (<code>source/auth/TokenUpdater.ts</code>):</strong>
Central browser auth state machine. State enum: <code>valid | refreshing | expired | network-error</code>,
exposed via <code>EventSlot</code>. Triggers: a 5-minute periodic timer and
<code>ActivityAnalyser.onActive</code> (fires on mouse, touch, focus, and tab-visibility
restore). Each trigger pings <code>GET /api/auth/me</code>; <code>jwtMiddleware</code>
handles proactive cookie rotation server-side when within 15 min of expiry
(access token is <code>httpOnly</code>, so expiry is unreadable client-side).
Web Locks leader election: the first tab acquires <code>roject-token-updater-leader</code>
exclusively and broadcasts state to followers via <code>BroadcastChannel</code>.
Leader handoff is automatic when the holder tab closes.
</p>
<p style="margin-top:0.75rem">
<strong>GuardedCall (<code>source/auth/GuardedCall.ts</code>):</strong>
Singleton wrapper for all API calls. Three tiers —
<code>user</code>: no retry, throws on failure;
<code>editor</code>: 3 retries at 1 s / 3 s / 8 s, then throws;
<code>silent</code>: same delays, never throws, <code>console.warn</code> on failure.
Pre-flight: blocks when state is <code>expired</code>; waits up to 6 s when
<code>refreshing</code>. Wired into <code>Editor.ts</code> (save = user, open = editor)
and <code>editor-shell.ts</code> (layout save/load = silent).
<strong>Token refresh:</strong>
rokojori-auth's page-level middleware (before <code>express.static</code>)
verifies the <code>accessToken</code> cookie but calls <code>next()</code> on
any error — no redirect on expiry. Transparent refresh for API calls is handled
by Roject's <code>jwtMiddleware</code>: when a <code>TokenExpiredError</code>
hits an <code>/api/</code> route and a <code>refreshToken</code> cookie is present,
it calls <code>POST AUTH_INTERNAL_HOST/api/auth/refresh</code> server-side, sets
the new cookies on the response, decodes the new JWT into <code>req.user</code>,
and continues transparently. If refresh fails, the request falls through to
<code>requireAuth</code> which returns 401. <code>AUTH_INTERNAL_HOST</code>
defaults to <code>AUTH_HOST</code>; set it to <code>http://localhost:3001</code>
in production to bypass nginx. The <code>editor-shell</code> checks
<code>GET /api/auth/me</code> on startup and redirects to <code>/</code> on 401.
</p>
<p style="margin-top:0.75rem">
<strong>Token extraction order (<code>extractToken</code> in <code>auth.ts</code>):</strong>
Bearer header is checked before the <code>accessToken</code> cookie. An explicit
<code>Authorization: Bearer </code> header always wins — critical for Electron
(which injects tokens via <code>onBeforeSendHeaders</code>) and any context where a
stale cookie might shadow a fresh token.
Bearer header is checked before the <code>accessToken</code> cookie. This means an
explicit <code>Authorization: Bearer ...</code> header always wins — critical for
Electron (which injects tokens via <code>onBeforeSendHeaders</code>), API clients,
and any context where a stale browser cookie might otherwise shadow a fresh token.
</p>
<p style="margin-top:0.75rem">
<strong>Clock skew — <code>JWT_CLOCK_TOLERANCE</code>:</strong>
<code>jwt.verify</code> accepts a <code>clockTolerance</code> option (seconds).
Set <code>JWT_CLOCK_TOLERANCE=7200</code> in <code>.env</code> 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.
</p>
<p style="margin-top:0.75rem">
<strong>New-session endpoint (<code>POST /api/auth/new-session</code>):</strong>
<code>requireAuth</code>-guarded; mints a fresh independent token pair via
<code>issueTokenPair</code> for the authenticated user. Used by Electron to start
a new independent session from a running instance's heartbeat without re-login.
The env var <code>JWT_CLOCK_TOLERANCE</code> (default 0 / unset) is read as an
integer and passed as <code>clockTolerance</code> when non-zero. Set to
<code>7200</code> in <code>.env</code> 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.
</p>
</div>
@ -464,27 +445,6 @@
<code>session.webRequest.onBeforeSendHeaders</code>. Run with
<code>npm run electron:dev</code>.
</p>
<p style="margin-top:0.75rem">
<strong>Electron token updater:</strong> runs in <code>electron/main.ts</code>.
Reads the access token's <code>exp</code> directly from the JWT payload (token is
held in the main process, not behind an <code>httpOnly</code> cookie). Uses a
server clock offset derived from the <code>Date</code> response header of the first
auth-server call (<code>_serverClockOffsetMs</code>) 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.
</p>
<p style="margin-top:0.75rem">
<strong>Heartbeat session sharing:</strong> a running instance writes
<code>{ accessToken, timestamp }</code> to <code>userData/session-heartbeat.json</code>
every 10 s. A newly-starting instance reads it on launch; if ≤ 30 s old, it calls
<code>POST /api/auth/new-session</code> (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
<code>last-password.txt</code> auto-login.
</p>
<p style="margin-top:0.75rem">
<strong>Local filesystem access:</strong> when <code>ROJECT_ELECTRON=true</code>,
the server mounts <code>/api/local/</code> routes backed by Node.js <code>fs</code>
@ -593,22 +553,11 @@
<code>build/app/library-ts/browser/</code>. The node part is included by
<code>tsconfig.ts-node.json</code>.
</p>
<p style="margin-top:0.75rem">
<strong>Import extension convention:</strong> all relative imports in
<code>browser/</code> use explicit <code>.js</code> extensions
(e.g. <code>from "../events/EventSlot.js"</code>). 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 <code>.js</code> files. TypeScript with
<code>moduleResolution: "bundler"</code> accepts <code>.js</code> extensions in
source even when the source file is <code>.ts</code>.
</p>
<div class="tags">
<span class="tag">git submodule</span>
<span class="tag">source/library-ts/</span>
<span class="tag">project references</span>
<span class="tag">composite: true</span>
<span class="tag">.js extensions required</span>
</div>
</div>