docs: auth overhaul, library-ts ESM fix, rokojori-auth testing task
This commit is contained in:
parent
6bcb48e331
commit
9327017da7
|
|
@ -22,6 +22,24 @@
|
||||||
<div class="lane">
|
<div class="lane">
|
||||||
<div class="lane-header">To Do</div>
|
<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-item class="blue hide-content">
|
||||||
<task-title>Rojo Chat: LLM Tools</task-title>
|
<task-title>Rojo Chat: LLM Tools</task-title>
|
||||||
<task-content>
|
<task-content>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<header>
|
<header>
|
||||||
<p class="date">Saturday, 2 August 2026</p>
|
<p class="date">Saturday, 2 August 2026</p>
|
||||||
<h1>Session History</h1>
|
<h1>Session History</h1>
|
||||||
<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>
|
<p class="subtitle">Rojo bug fixes, Claude API and Claude Code providers, auth session refresh overhaul (7 phases), library-ts ESM import fix.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -118,6 +118,55 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3><a href="2026/08-August/02-Saturday/index.html">Saturday, 2 August 2026</a></h3>
|
<h3><a href="2026/08-August/02-Saturday/index.html">Saturday, 2 August 2026</a></h3>
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -45,38 +45,57 @@
|
||||||
<p>
|
<p>
|
||||||
User accounts are managed by <strong>rokojori-auth</strong> at
|
User accounts are managed by <strong>rokojori-auth</strong> at
|
||||||
<code>account.rokojori.com</code> — registration, login, JWT issuance, refresh
|
<code>account.rokojori.com</code> — registration, login, JWT issuance, refresh
|
||||||
token rotation, roles, and account deletion.
|
token rotation with a grace window, roles, and account deletion.
|
||||||
</p>
|
</p>
|
||||||
<p style="margin-top:0.75rem">
|
<p style="margin-top:0.75rem">
|
||||||
<strong>Token refresh:</strong>
|
<strong>Refresh token grace window (rokojori-auth):</strong>
|
||||||
rokojori-auth's page-level middleware (before <code>express.static</code>)
|
<code>refreshTokens</code> are soft-deleted — <code>usedAt</code> and
|
||||||
verifies the <code>accessToken</code> cookie but calls <code>next()</code> on
|
<code>replacedBy</code> are stamped on first use rather than the row being deleted.
|
||||||
any error — no redirect on expiry. Transparent refresh for API calls is handled
|
A concurrent second refresh within <code>REFRESH_GRACE_TTL</code> (10 s) resolves
|
||||||
by Roject's <code>jwtMiddleware</code>: when a <code>TokenExpiredError</code>
|
to the same replacement pair instead of 401ing. This is the baseline correctness
|
||||||
hits an <code>/api/</code> route and a <code>refreshToken</code> cookie is present,
|
guarantee against races; client-side leader election is an optimisation on top.
|
||||||
it calls <code>POST AUTH_INTERNAL_HOST/api/auth/refresh</code> server-side, sets
|
</p>
|
||||||
the new cookies on the response, decodes the new JWT into <code>req.user</code>,
|
<p style="margin-top:0.75rem">
|
||||||
and continues transparently. If refresh fails, the request falls through to
|
<strong>TokenUpdater (<code>source/auth/TokenUpdater.ts</code>):</strong>
|
||||||
<code>requireAuth</code> which returns 401. <code>AUTH_INTERNAL_HOST</code>
|
Central browser auth state machine. State enum: <code>valid | refreshing | expired | network-error</code>,
|
||||||
defaults to <code>AUTH_HOST</code>; set it to <code>http://localhost:3001</code>
|
exposed via <code>EventSlot</code>. Triggers: a 5-minute periodic timer and
|
||||||
in production to bypass nginx. The <code>editor-shell</code> checks
|
<code>ActivityAnalyser.onActive</code> (fires on mouse, touch, focus, and tab-visibility
|
||||||
<code>GET /api/auth/me</code> on startup and redirects to <code>/</code> on 401.
|
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).
|
||||||
</p>
|
</p>
|
||||||
<p style="margin-top:0.75rem">
|
<p style="margin-top:0.75rem">
|
||||||
<strong>Token extraction order (<code>extractToken</code> in <code>auth.ts</code>):</strong>
|
<strong>Token extraction order (<code>extractToken</code> in <code>auth.ts</code>):</strong>
|
||||||
Bearer header is checked before the <code>accessToken</code> cookie. This means an
|
Bearer header is checked before the <code>accessToken</code> cookie. An explicit
|
||||||
explicit <code>Authorization: Bearer ...</code> header always wins — critical for
|
<code>Authorization: Bearer …</code> header always wins — critical for Electron
|
||||||
Electron (which injects tokens via <code>onBeforeSendHeaders</code>), API clients,
|
(which injects tokens via <code>onBeforeSendHeaders</code>) and any context where a
|
||||||
and any context where a stale browser cookie might otherwise shadow a fresh token.
|
stale cookie might shadow a fresh token.
|
||||||
</p>
|
</p>
|
||||||
<p style="margin-top:0.75rem">
|
<p style="margin-top:0.75rem">
|
||||||
<strong>Clock skew — <code>JWT_CLOCK_TOLERANCE</code>:</strong>
|
<strong>Clock skew — <code>JWT_CLOCK_TOLERANCE</code>:</strong>
|
||||||
<code>jwt.verify</code> accepts a <code>clockTolerance</code> option (seconds).
|
<code>jwt.verify</code> accepts a <code>clockTolerance</code> option (seconds).
|
||||||
The env var <code>JWT_CLOCK_TOLERANCE</code> (default 0 / unset) is read as an
|
Set <code>JWT_CLOCK_TOLERANCE=7200</code> in <code>.env</code> for local dev to
|
||||||
integer and passed as <code>clockTolerance</code> when non-zero. Set to
|
absorb skew between the Windows dev machine and the production auth server. Never
|
||||||
<code>7200</code> in <code>.env</code> for local development to absorb clock skew
|
set this in production — fix the clock instead.
|
||||||
between the Windows dev machine and the production auth server. Never set this in
|
</p>
|
||||||
production — if you need it there, fix the clock instead.
|
<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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -445,6 +464,27 @@
|
||||||
<code>session.webRequest.onBeforeSendHeaders</code>. Run with
|
<code>session.webRequest.onBeforeSendHeaders</code>. Run with
|
||||||
<code>npm run electron:dev</code>.
|
<code>npm run electron:dev</code>.
|
||||||
</p>
|
</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">
|
<p style="margin-top:0.75rem">
|
||||||
<strong>Local filesystem access:</strong> when <code>ROJECT_ELECTRON=true</code>,
|
<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>
|
the server mounts <code>/api/local/</code> routes backed by Node.js <code>fs</code>
|
||||||
|
|
@ -553,11 +593,22 @@
|
||||||
<code>build/app/library-ts/browser/</code>. The node part is included by
|
<code>build/app/library-ts/browser/</code>. The node part is included by
|
||||||
<code>tsconfig.ts-node.json</code>.
|
<code>tsconfig.ts-node.json</code>.
|
||||||
</p>
|
</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">
|
<div class="tags">
|
||||||
<span class="tag">git submodule</span>
|
<span class="tag">git submodule</span>
|
||||||
<span class="tag">source/library-ts/</span>
|
<span class="tag">source/library-ts/</span>
|
||||||
<span class="tag">project references</span>
|
<span class="tag">project references</span>
|
||||||
<span class="tag">composite: true</span>
|
<span class="tag">composite: true</span>
|
||||||
|
<span class="tag">.js extensions required</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue