Skip to content

feat(server,mac): no-auth mode for embedded server (LOBU_NO_AUTH=1)#779

Merged
buremba merged 3 commits into
mainfrom
feat/no-auth-server-mode
May 16, 2026
Merged

feat(server,mac): no-auth mode for embedded server (LOBU_NO_AUTH=1)#779
buremba merged 3 commits into
mainfrom
feat/no-auth-server-mode

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 16, 2026

Summary

Replaces closed PR #777 ("lift the bootstrap PAT") with the cleaner Phase B from `docs/plans/personal-mode-auth.md`. The server short-circuits auth when `LOBU_NO_AUTH=1` and attributes every request to the local user that `ensureBootstrapPat` already creates on first boot. The macOS menu bar spawns the runner with that env, then synthesises credentials directly — no PAT to read, no verification call, no ownership tracking.

Why this over the PAT-lifting approach

Pi's iterative review of PR #777 surfaced enough corners in the lift-the-token frame (server-supplied URL exfiltration, PAT-not-accepted by /userinfo, squatter on :8787 receiving the bearer, file-vs-DB consistency, save-failure handling) that the no-token frame is less code overall and has a cleaner mental model: "server in personal mode treats every request as the local user, period."

Server changes

  • `multi-tenant.ts` — new `getNoAuthUser()` helper loads the bootstrap user + their personal org once and caches; `resolveAuth()` short-circuits with owner-role attribution when `LOBU_NO_AUTH=1`. URL-supplied org slug must match the local user's org (no-auth mode is single-org by definition).
  • `start-local.ts` — post-listen `server.address()` assertion refuses to serve on anything other than `127.0.0.1` / `::1` when `LOBU_NO_AUTH=1`. Pre-listen `HOST` validation as a secondary guard.

Mac app changes

  • `LocalLobuRunner` — sets `LOBU_NO_AUTH=1` in the spawn env.
  • `AppState.connect()` — managed-runner path calls `adoptLocalCredentials()` with synthesised `OAuthCredentials` (dummy bearer the server ignores). No PAT file read, no `/userinfo` verification, no `spawnedThisSession` check.
  • `MenuBarContent` button — "Start" / "Connect" instead of "Start & sign in."

Test plan

  • Fresh install → click Start → server spawns → popover transitions to signed-in within ~1 s. No browser opens.
  • Quit + relaunch → still signed in (credentials persisted).
  • Edit URL to `https://app.lobu.ai\` → button reads "Sign in" → click → normal OAuth flow.
  • Edit URL to `http://localhost:9999\` → button reads "Sign in" (not Start) → click → OAuth attempt (no auto-spawn).
  • `HOST=0.0.0.0 LOBU_NO_AUTH=1 lobu run` → refuses to start with clear error.
  • `LOBU_NO_AUTH=1 lobu run` against fresh DATA_DIR → wait a moment → API calls succeed without any `Authorization` header.

Defers (still in design doc)

  • CSRF middleware on mutating routes (`Origin` / `Sec-Fetch-Site` / `Host` / `Content-Type`). Browser-tab exfiltration risk remains until shipped — today no-auth mode trusts that loopback bind is the only attack surface.
  • Per-user data dir / port for shared macOS user accounts.

Summary by CodeRabbit

  • New Features

    • Add a no-auth connection mode for embedded local runners that permits connecting without interactive sign-in when the runner was started in this session.
    • Connection button now shows “Start” or “Connect” (not “Start & sign in”) for managed local runner flows.
    • Menubar client now tags requests with a client identifier header.
  • Security

    • No-auth mode restricted to loopback-only and enforces origin/host checks for mutating requests.
  • Bug Fixes

    • More reliable local runner startup and session ownership tracking.

Review Change Stack

Replaces the closed PR #777 ("lift the bootstrap PAT") with the cleaner
Phase B from docs/plans/personal-mode-auth.md: server short-circuits
auth when LOBU_NO_AUTH=1, attributes every request to the local user
ensureBootstrapPat() seeded. The macOS menu bar spawns the runner with
that env set, then sets credentials directly — no PAT to read, no
verification call, no ownership tracking.

Server (packages/server):
- multi-tenant.ts: getNoAuthUser() loads the bootstrap-user + their
  personal org once and caches; resolveAuth() short-circuits with
  owner-role attribution when LOBU_NO_AUTH=1. URL-supplied org slug
  must match the local user's org (single-org by definition).
- start-local.ts: post-listen bind assertion refuses to serve on
  anything other than 127.0.0.1 / ::1 when LOBU_NO_AUTH=1. Surfaces a
  hard error early instead of silently exposing the local user's data.

Mac app (apps/mac/Lobu):
- LocalLobuRunner sets LOBU_NO_AUTH=1 in the spawn env alongside
  LOBU_DATA_DIR.
- AppState.connect() — when targeting the managed runner, calls
  adoptLocalCredentials() with synthesised OAuthCredentials (dummy
  bearer; server ignores it). No PAT file, no userinfo verification,
  no spawnedThisSession check — none of that is needed when the
  server itself bypasses auth.
- MenuBarContent button reads "Start" / "Connect" for managed-runner
  URLs.

User experience: click Start once. Server spawns with no-auth env,
popover transitions to signed-in within ~1 s. No browser, no code, no
approval. The dummy bearer the menu bar sends is never validated; it
exists only so the existing WorkerClient/Authorization scaffolding
doesn't have to learn a "no header" mode.

Defers (still in docs/plans/personal-mode-auth.md):
- CSRF middleware on mutating routes — browser-tab exfiltration risk
  remains until we ship Origin / Sec-Fetch-Site / Host / Content-Type
  checks. Today no-auth mode trusts that the loopback bind is the
  only attack surface.
- Per-user data dir / port for shared macOS user accounts.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e0b4f11-baa0-4918-a654-04b69489899b

📥 Commits

Reviewing files that changed from the base of the PR and between d6126b1 and cca1ac3.

📒 Files selected for processing (6)
  • apps/mac/Lobu/AppState.swift
  • apps/mac/Lobu/ChromeBridgeHost.swift
  • apps/mac/Lobu/LobuClient.swift
  • apps/mac/Lobu/OAuthClient.swift
  • packages/server/src/index.ts
  • packages/server/src/start-local.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/mac/Lobu/AppState.swift
  • packages/server/src/index.ts
  • packages/server/src/start-local.ts
  • apps/mac/Lobu/LobuClient.swift

📝 Walkthrough

Walkthrough

The PR implements a loopback-only no-auth mode for the embedded local Lobu runner: the macOS app detects managed local-runner URLs and adopts synthesized no-auth credentials (persisted), the runner is started with LOBU_NO_AUTH=1 and tracked per-session, client requests include X-Lobu-Client: menubar, and the server enforces loopback startup, CSRF-like checks, and resolves a bootstrap user for requests.

Changes

No-auth Local Runner

Layer / File(s) Summary
macOS client no-auth flow initiation
apps/mac/Lobu/AppState.swift, apps/mac/Lobu/MenuBarContent.swift
AppState validates stored no-auth credentials against spawnedThisSession, documents embedded-server handling, sets serverMode/baseURL for managed-runner URLs, and adopts synthesized no-auth credentials; menu button labels show "Start" or "Connect".
Local runner spawn & session tracking
apps/mac/Lobu/LocalLobuRunner.swift
LocalLobuRunner.start() sets LOBU_NO_AUTH=1 and forces HOST=127.0.0.1 for the child lobu run process, and adds spawnedThisSession: Bool to record per-session spawns; flag cleared on failure/stop.
Client request identification header
apps/mac/Lobu/LobuClient.swift, apps/mac/Lobu/ChromeBridgeHost.swift, apps/mac/Lobu/OAuthClient.swift
Adds X-Lobu-Client: menubar to native client GET/POST/PATCH/DELETE helpers and mint-child-token so native requests can be recognized by server middleware during no-auth mode.
Server start-local: bootstrap & loopback enforcement
packages/server/src/start-local.ts, packages/server/src/utils/loopback.ts, packages/server/src/server.ts
Run bootstrap steps before listening (with warnings on failure), rework bootstrap-PAT logic to consult DB state, add isLoopbackHost() and enforce loopback-only startup when LOBU_NO_AUTH=1 (pre-listen HOST check and post-listen bound-address check).
Server CSRF middleware & no-auth auth resolution
packages/server/src/index.ts, packages/server/src/workspace/multi-tenant.ts
Add middleware active under LOBU_NO_AUTH=1 that blocks mutating requests unless Host/Origin/sec-fetch-site or x-lobu-client checks pass and Content-Type is JSON; add getNoAuthUser() and an early no-auth branch in MultiTenantProvider.resolveAuth that returns 503 if bootstrap user missing, 403 on org mismatch, or populates request context with the bootstrap user.

Sequence Diagram

sequenceDiagram
  participant User
  participant MacApp as Mac App (AppState)
  participant LocalRunner as LocalLobuRunner
  participant Server as Lobu Server
  participant Auth as MultiTenant Auth
  User->>MacApp: open managed local runner URL
  MacApp->>MacApp: detect managed URL, adoptLocalCredentials()
  MacApp->>LocalRunner: start lobu run with LOBU_NO_AUTH=1
  LocalRunner->>Server: child process binds and serves
  Server->>Server: isLoopbackHost validation (pre/post-listen)
  Server->>Auth: incoming request routed to resolveAuth()
  Auth->>Auth: getNoAuthUser() -> populate auth context
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • lobu-ai/lobu#773: Updates ChromeBridgeHost.swift / related POST helpers to set X-Lobu-Client: menubar, matching this PR's CSRF/native-client header changes.
  • lobu-ai/lobu#774: Earlier work adjusting AppState.connect() and UX to detect/manage embedded local-runner URLs; this PR extends that flow with no-auth credential adoption and server enforcement.

Suggested labels

skip-size-check

Poem

🐰 In loopback burrows, servers wake,
No tokens needed for this little break,
The client hops in, a header in paw,
The runner springs up on localhost law,
The rabbit hums: "Start, connect, and play!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely describes the main feature: no-auth mode for embedded server, activated via LOBU_NO_AUTH=1 environment variable.
Description check ✅ Passed Description provides comprehensive coverage of changes across files, explains design rationale, documents test scenarios, and clearly marks deferred work; matches template structure with Summary and Test plan sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/no-auth-server-mode

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23f19e78b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// No-auth mode: server attributes every request to the local user
// without an OAuth flow, a token, or a sign-in screen. Personal-use
// model — see docs/plans/personal-mode-auth.md for the threat model.
env["LOBU_NO_AUTH"] = "1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Set HOST when enabling no-auth

When the mac app spawns lobu run it now sets LOBU_NO_AUTH=1, but it never overrides the embedded server's default HOST, which is still 0.0.0.0 in packages/server/src/start-local.ts. The new no-auth startup guard rejects any non-loopback host, so the normal Start path (lobu run --port 8787 from the menu bar with no HOST env) exits before becoming ready instead of launching the local server; set HOST=127.0.0.1 here or make the server default loopback in no-auth mode.

Useful? React with 👍 / 👎.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/mac/Lobu/AppState.swift (1)

402-417: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Require a positive no-auth signal before persisting dummy credentials.

matchesManagedRunner() only proves “this is localhost:8787”. LocalLobuRunner.start() will also adopt any already-running Lobu on that port, so this branch can skip OAuth against a normal auth-enabled server and save noauth credentials anyway. From there, poll/search/notifications all talk to the wrong auth model and fail. Only take this path after the server proves it is running in no-auth mode, or after attaching to a process this app spawned itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/mac/Lobu/AppState.swift` around lines 402 - 417, The code assumes
matchesManagedRunner(url) implies a no-auth server and unconditionally calls
adoptLocalCredentials and sets serverMode to .local; instead require a positive
no-auth confirmation or proof we spawned the process before persisting dummy
credentials. Modify the branch around matchesManagedRunner/startLocalLobu: after
startLocalLobu or when localLobuStatus.isRunning is true, perform a lightweight
probe (e.g., call an auth-check endpoint or implement isNoAuthServer(url)) or
check a flag that indicates the runner was started by our process (e.g.,
localLobuStatus.startedByUs); only if that probe returns no-auth OR startedByUs
is true, call adoptLocalCredentials(baseURL:) and set serverMode = .local and
startAutoPollIfSignedIn(); otherwise treat it as .remote and do not persist
noauth credentials.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server/src/start-local.ts`:
- Around line 197-223: When LOBU_NO_AUTH=1 is enabled the process currently
exits if HOST is not loopback; instead detect noAuth (process.env.LOBU_NO_AUTH
=== '1') and, if HOST is unset or is the non-loopback default (e.g. '0.0.0.0' /
'::'), set HOST to a loopback address (e.g. '127.0.0.1') before calling
httpServer.listen so the documented env-only flow works; retain the
isLoopbackHost(addr.address) post-listen safety check and use the same effective
HOST value for the listen() call and logger messages (references: HOST, noAuth,
isLoopbackHost, httpServer.listen, logger, PORT, DATA_DIR).

In `@packages/server/src/workspace/multi-tenant.ts`:
- Around line 122-150: getNoAuthUser currently picks the first owner/admin
membership for 'bootstrap-user' which can return the wrong org; narrow the query
to the seeded personal organization created by ensureBootstrapPat(). Modify
getNoAuthUser (or its call sites) to obtain the bootstrap personal org id (via
ensureBootstrapPat() or a helper that returns the seeded org id) and add a
filter m."organizationId" = <bootstrapPersonalOrgId> (or join the organization
table and filter to the seeded personal org) so the query only returns the
bootstrap-user row for the intended personal org; update the signature of
getNoAuthUser if needed to accept that org id and adjust callers accordingly.

---

Outside diff comments:
In `@apps/mac/Lobu/AppState.swift`:
- Around line 402-417: The code assumes matchesManagedRunner(url) implies a
no-auth server and unconditionally calls adoptLocalCredentials and sets
serverMode to .local; instead require a positive no-auth confirmation or proof
we spawned the process before persisting dummy credentials. Modify the branch
around matchesManagedRunner/startLocalLobu: after startLocalLobu or when
localLobuStatus.isRunning is true, perform a lightweight probe (e.g., call an
auth-check endpoint or implement isNoAuthServer(url)) or check a flag that
indicates the runner was started by our process (e.g.,
localLobuStatus.startedByUs); only if that probe returns no-auth OR startedByUs
is true, call adoptLocalCredentials(baseURL:) and set serverMode = .local and
startAutoPollIfSignedIn(); otherwise treat it as .remote and do not persist
noauth credentials.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dd85295-6832-4e15-bd5a-7230e11978a9

📥 Commits

Reviewing files that changed from the base of the PR and between b3844b6 and 23f19e7.

📒 Files selected for processing (5)
  • apps/mac/Lobu/AppState.swift
  • apps/mac/Lobu/LocalLobuRunner.swift
  • apps/mac/Lobu/MenuBarContent.swift
  • packages/server/src/start-local.ts
  • packages/server/src/workspace/multi-tenant.ts

Comment on lines +197 to 223
// No-auth mode is loopback-only by design. Refuse to listen on anything
// other than 127.0.0.1 / ::1 — both via the configured HOST (early fail)
// and via a post-listen `server.address()` check that catches surprises
// (DNS resolution, hostname aliasing, etc.).
const noAuth = process.env.LOBU_NO_AUTH === '1';
if (noAuth && !isLoopbackHost(HOST)) {
logger.error(
{ host: HOST },
'LOBU_NO_AUTH=1 requires loopback bind (127.0.0.1 or ::1). Refusing to start.'
);
process.exit(1);
}
httpServer.listen(PORT, HOST, () => {
if (noAuth) {
const addr = httpServer.address();
if (typeof addr === 'object' && addr && !isLoopbackHost(addr.address)) {
logger.error(
{ address: addr.address },
'LOBU_NO_AUTH=1 server bound to a non-loopback address after listen() — refusing to serve.'
);
process.exit(1);
}
}
logger.info(`Lobu running at http://${HOST}:${PORT}`);
logger.info(`Data: ${DATA_DIR}`);
if (noAuth) logger.info('No-auth mode active (LOBU_NO_AUTH=1) — every request attributed to local user');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

LOBU_NO_AUTH=1 currently rejects the default startup path.

HOST still falls back to 0.0.0.0 above, so enabling no-auth without also setting HOST exits here before listen(). That blocks the documented env-only flow and the new macOS launcher path unless another layer knows about this extra contract. Default the no-auth case to a loopback bind, then use that same effective host for listen() and logging.

💡 Suggested fix
-  const noAuth = process.env.LOBU_NO_AUTH === '1';
-  if (noAuth && !isLoopbackHost(HOST)) {
+  const noAuth = process.env.LOBU_NO_AUTH === '1';
+  const effectiveHost = noAuth && !process.env.HOST?.trim() ? '127.0.0.1' : HOST;
+  if (noAuth && !isLoopbackHost(effectiveHost)) {
     logger.error(
-      { host: HOST },
+      { host: effectiveHost },
       'LOBU_NO_AUTH=1 requires loopback bind (127.0.0.1 or ::1). Refusing to start.'
     );
     process.exit(1);
   }
-  httpServer.listen(PORT, HOST, () => {
+  httpServer.listen(PORT, effectiveHost, () => {
     if (noAuth) {
       const addr = httpServer.address();
       if (typeof addr === 'object' && addr && !isLoopbackHost(addr.address)) {
         logger.error(
           { address: addr.address },
@@
-    logger.info(`Lobu running at http://${HOST}:${PORT}`);
+    logger.info(`Lobu running at http://${effectiveHost}:${PORT}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/start-local.ts` around lines 197 - 223, When
LOBU_NO_AUTH=1 is enabled the process currently exits if HOST is not loopback;
instead detect noAuth (process.env.LOBU_NO_AUTH === '1') and, if HOST is unset
or is the non-loopback default (e.g. '0.0.0.0' / '::'), set HOST to a loopback
address (e.g. '127.0.0.1') before calling httpServer.listen so the documented
env-only flow works; retain the isLoopbackHost(addr.address) post-listen safety
check and use the same effective HOST value for the listen() call and logger
messages (references: HOST, noAuth, isLoopbackHost, httpServer.listen, logger,
PORT, DATA_DIR).

Comment thread packages/server/src/workspace/multi-tenant.ts
buremba added 2 commits May 17, 2026 00:34
Server (packages/server):
- HOST default of 0.0.0.0 conflicted with the no-auth bind guard. The Mac
  runner now explicitly sets HOST=127.0.0.1 in the spawn env (#1), and
  server.ts also gets the same loopback bind guard so an accidental
  LOBU_NO_AUTH=1 in production refuses to start instead of silently
  bypassing auth on the public bind (#5).

- ensureBootstrapPat now runs BEFORE httpServer.listen so the very first
  request can't 503 due to the seeding race (#3). The early-return now
  trusts the DB row, not the bootstrap-pat.txt file: a wiped LOBU_DATA_DIR
  with a leftover file used to leave no-auth permanently 503; now we
  re-mint the user/org/PAT (#4). Production safety guard still skips when
  OTHER (non-bootstrap) users exist.

- getNoAuthUser pins to the BOOTSTRAP_USER_ID + BOOTSTRAP_ORG_ID pair
  directly, not "first owner/admin membership LIMIT 1" — eliminates the
  nondeterminism if bootstrap-user ever gets cross-org memberships (#7).

- isLoopbackHost moved to packages/server/src/utils/loopback.ts so both
  start-local and server share it. Handles the full IPv4 loopback /8,
  ::1, [::1], and IPv4-mapped IPv6 loopback (::ffff:127.x.y.z) (#8).

- New CSRF middleware in index.ts. Only fires when LOBU_NO_AUTH=1. On
  mutating methods (POST/PUT/PATCH/DELETE) requires:
    * Host header is a loopback alias (defeats DNS rebinding).
    * Origin or Sec-Fetch-Site says same-origin/none, OR a custom
      X-Lobu-Client header is present (native clients omit Origin).
    * Content-Type, if set, must be application/json — defeats CSRF
      simple-request form posts that browsers allow without preflight.
  This is the only protection between the no-auth bypass and any
  malicious site the user visits in their browser, so it MUST be on
  whenever no-auth is on (#6).

Mac app (apps/mac/Lobu):
- LocalLobuRunner: pass HOST=127.0.0.1 alongside LOBU_NO_AUTH=1, and
  restore spawnedThisSession tracking so adoptLocalCredentials can refuse
  adoption when start() adopted a pre-existing server instead of
  spawning one. A malicious squatter or someone else's lobu run would
  otherwise receive our synthesised credentials (#2).
- WorkerClient sends X-Lobu-Client: menubar on every request so the new
  CSRF middleware accepts native client traffic that legitimately omits
  Origin (#6).
…RF holes

Pi's verification of the previous fixup commit caught three remaining
issues:

1. AppState's init re-spawns the runner and starts polling using the
   persisted no-auth credentials WITHOUT checking spawnedThisSession.
   If a squatter happened to win :8787 on startup, the polling client
   would send our synthesised "noauth" bearer + X-Lobu-Client to it.
   Now: after startLocalLobu in init, if we didn't actually spawn the
   process, clear the persisted creds and stop. The user will see the
   connection card again and can sign in via OAuth.

2. ensureBootstrapPat checked only the user row's existence before the
   early-return. Partial state (user exists, org or member rows missing)
   would still wedge getNoAuthUser forever. Now we check all three rows
   together — any missing one triggers a re-mint.

3. CSRF middleware: tightened in three ways.
   - Missing Content-Type on a mutation is now rejected (was previously
     a bypass — `if (ct && ...)` skipped when ct was empty).
   - WorkerClient.markNotificationRead now sends Content-Type:
     application/json even with an empty body to satisfy the tightened
     check.
   - OAuthClient.postRawJSON and ChromeBridgeHost.mintChildToken now
     send X-Lobu-Client: menubar so they aren't rejected for missing
     Origin in no-auth mode.
   - Host header validation reuses the shared isLoopbackHost util
     (stripping port + brackets first) so the alias set is consistent
     with the bind-time enforcement.
@buremba buremba merged commit a3e6f0a into main May 16, 2026
20 of 22 checks passed
@buremba buremba deleted the feat/no-auth-server-mode branch May 16, 2026 23:52
buremba added a commit that referenced this pull request May 17, 2026
PR #779 introduced no-auth mode but left three rough edges in the Mac
app side of the spawn:

- HOST=127.0.0.1 wasn't set in the runner env. start-local.ts defaults
  HOST=0.0.0.0, and the no-auth bind guard refuses non-loopback, so
  Start would silently crash the runner on boot.

- ENCRYPTION_KEY isn't set on personal installs and the embedded gateway
  refuses to boot without it (or LOBU_ALLOW_EPHEMERAL_ENCRYPTION_KEY=1).
  For a personal install the ephemeral key is the only sensible default
  — opt in here so the user doesn't manage a secret manually.

- The bundled lobu CLI requires Node 22.x–24.x (isolated-vm constraint)
  but a user on Node 25+ via Homebrew's `node` formula would crash the
  runner. Prefer well-known Node 22 install paths (Homebrew keg-only
  `node@22`, mise, fnm) by prepending them to PATH for the spawn.

Also cleans up two related UX issues:

- AppState.connect() now auto-fires on launch when credentials are nil
  and the URL targets the managed runner — no Start button click needed
  for the 99% case. The connection card only surfaces on actual failure
  (CLI missing, port conflict, etc).

- startLocalLobu()'s failure handler no longer echoes the runner error
  into state.status. The connection card's connectStatusLine already
  shows it in orange under the button; echoing into state.status
  duplicates the message at the top of the popover.
buremba added a commit that referenced this pull request May 17, 2026
The conflict-update early-return path called upsertEmbedding(existing.id, ...)
before the SELECT that confirms the row is still there. If the row got
deleted/tombstoned in that window, upsertEmbedding would FK-fail with a
confusing error instead of letting us fall through to a fresh insert.

Reorder so the embedding upsert only fires when the reread confirms the
event still exists. Fall-through behavior on the race is unchanged.

The 3 other CodeRabbit comments on this PR were against code from PR #779
(now merged) — those will land as separate follow-ups since they're out
of scope here.
buremba added a commit that referenced this pull request May 17, 2026
The conflict-update early-return path called upsertEmbedding(existing.id, ...)
before the SELECT that confirms the row is still there. If the row got
deleted/tombstoned in that window, upsertEmbedding would FK-fail with a
confusing error instead of letting us fall through to a fresh insert.

Reorder so the embedding upsert only fires when the reread confirms the
event still exists. Fall-through behavior on the race is unchanged.

The 3 other CodeRabbit comments on this PR were against code from PR #779
(now merged) — those will land as separate follow-ups since they're out
of scope here.
buremba added a commit that referenced this pull request May 17, 2026
* fix(insert-event): guard against empty INSERT RETURNING

`insertEvent` assumed `INSERT ... RETURNING` always yields a row, then
dereferenced `result[0].id` to feed `upsertEmbedding`. When the result
came back empty — exposed by the menu bar's no-auth sync round-trip
completing for the first time — the call crashed with the cryptic
`TypeError: Cannot read properties of undefined (reading 'id')` and
the worker stream returned 500.

Two defensive guards:

1. After the main INSERT path, treat `result[0]` as possibly undefined.
   Log connector/feed/origin context and throw a real error explaining
   the row wasn't persisted. Callers (worker streamContent) catch the
   throw and surface a useful message instead of the stack-trace blob.

2. In the conflict-update early-return path, `existingRows[0]` could
   also be undefined if the found row was deleted/tombstoned between
   `findCurrentEventByOrigin` and the subsequent reread (race). Fall
   through to the fresh-insert path instead of crashing.

Root cause of the original empty-result is still under investigation —
likely a PGlite quirk with the new `search_tsv tsvector GENERATED ALWAYS
AS (...) STORED` column from PR #765, or a transient state during
bootstrap-org/connector wire-up. Either way, the right behavior is to
fail loud with context rather than crash on `undefined.id`.

* fix(insert-event): move embedding upsert after reread (CodeRabbit catch)

The conflict-update early-return path called upsertEmbedding(existing.id, ...)
before the SELECT that confirms the row is still there. If the row got
deleted/tombstoned in that window, upsertEmbedding would FK-fail with a
confusing error instead of letting us fall through to a fresh insert.

Reorder so the embedding upsert only fires when the reread confirms the
event still exists. Fall-through behavior on the race is unchanged.

The 3 other CodeRabbit comments on this PR were against code from PR #779
(now merged) — those will land as separate follow-ups since they're out
of scope here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants