Compare commits
No commits in common. "9327017da7e822d3a72a278db15ac6a2df2b249b" and "6f4d5c9ff201a47d8e63eb14b553b91fa2b3b49a" have entirely different histories.
9327017da7
...
6f4d5c9ff2
|
|
@ -1 +1 @@
|
|||
Subproject commit 3884dd4edb5dbaad8d4c5cb95837ee196dd8080b
|
||||
Subproject commit 7398efdfad12caec1ddf71968992543b0b936e76
|
||||
|
|
@ -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 >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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue