Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3170b9b
Merge remote-tracking branch 'origin/main' into fix/coderabbit-pr614-…
jeevanpillay Apr 24, 2026
e5c36f7
fix(desktop/auth): resolve CodeRabbit PR #614 review findings
jeevanpillay Apr 24, 2026
9e1c07d
test(desktop/auth): automate Phase 2 manual verification as vitest co…
jeevanpillay Apr 24, 2026
5081906
test(api/app): add expired-Bearer-no-cookie resolveClerkSession case
jeevanpillay Apr 24, 2026
6e83296
chore(desktop): upgrade electron 41, vite 8, plugin-react 6
jeevanpillay Apr 24, 2026
bbfaecb
chore(deps): address CodeRabbit PR #622 findings
jeevanpillay Apr 25, 2026
476d898
Merge pull request #622 from lightfastai/chore/desktop-deps-upgrade
jeevanpillay Apr 25, 2026
703a205
fix(desktop/windows): replace import.meta.url with __dirname for CJS …
jeevanpillay Apr 25, 2026
016f9ad
fix(app/proxy): allow /api/desktop/* through Clerk middleware
jeevanpillay Apr 25, 2026
b30d999
feat(desktop): custom URL scheme + PKCE sign-in flow
jeevanpillay Apr 25, 2026
cacc0ca
Merge branch 'main' into fix/coderabbit-pr614-followup
jeevanpillay Apr 26, 2026
f89a302
chore(deps): reconcile pnpm-lock catalog entries after merge
jeevanpillay Apr 26, 2026
716146c
Merge branch 'main' into fix/coderabbit-pr614-followup
jeevanpillay May 6, 2026
38ac764
fix(desktop): PR 627 review fixes — Sentry vendor wrap, blockers, Biome
jeevanpillay May 6, 2026
cf4438a
feat(desktop/auth): build sign-in URL via createAppUrl()
jeevanpillay May 6, 2026
884a9eb
fix(vendor/observability): commit missing Sentry wrapper modules
jeevanpillay May 6, 2026
6ffd137
docs(plan/pr627): record wrapper-files fix-up and green CI on 884a9eb97
jeevanpillay May 6, 2026
d1c0429
docs(plan/pr627): annotate Phase 5 partial live verification
jeevanpillay May 6, 2026
ad7d164
fix(desktop/auth): exchangeCode via createAppUrl(), drop legacy LIGHT…
jeevanpillay May 6, 2026
bf1699f
Merge branch 'main' into fix/coderabbit-pr614-followup
jeevanpillay May 6, 2026
060ac55
fix(pr627): address CodeRabbit T2 + T3 — vendor abstraction + test en…
jeevanpillay May 6, 2026
21c0eaa
fix(pr627): address CodeRabbit T4 — pin Sentry deps via catalog protocol
jeevanpillay May 6, 2026
fde860d
fix(pr627): T2 follow-up — sort imports and update test mock target
jeevanpillay May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .agents/skills/lightfast-desktop-signin/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
name: lightfast-desktop-signin
description: |
Sign the Lightfast desktop app in from an agent harness without opening Dia
or the system default browser. Drives a single agent-browser session over
a deterministic stdout JSON event grammar. Triggers when the user wants the
desktop app authed for tRPC/E2E/renderer testing, or asks to "sign the
desktop app in for me". Dev-only: refuses against pk_live_ Clerk keys or
non-localhost LIGHTFAST_API_URL.
---

# Lightfast Desktop Sign-In Skill

End-to-end agent runbook for getting the Electron desktop app signed in via
a custom URL scheme + PKCE flow. Replaces the older "log-grep loopback URL,
attach CDP to renderer" choreography with a single line of stdout JSON in,
single agent-browser session, structured completion event out.

## When to use

You need the desktop app to hold a real Clerk JWT — to drive tRPC procedures
that require `authedProcedure`, run E2E flows against signed-in renderer
surfaces, or smoke-test the auth mesh end-to-end. If you only need a JWT for
HTTP calls (not the desktop), use `lightfast-clerk` instead — it's faster.

## Preconditions

- Local dev mesh up on `:3024` (`pnpm dev:full` or `pnpm dev:app`).
- Clerk publishable key is `pk_test_*` (refuse `pk_live_*`).
- `LIGHTFAST_API_URL` either unset or pointing at `http://localhost:*` (refuse
any non-localhost host).
- `agent-browser` installed and reachable on PATH.
- The desktop app must already be running before you trigger the redirect.
Cold-launching via OS dispatch is unreliable in dev (unpackaged Electron
registers `lightfast-dev://` against `com.github.electron`, not Lightfast's
bundle id, so LaunchServices relaunches bare Electron without our entrypoint).
Packaged builds are fine; agents run unpackaged dev builds.

## Required environment

| Var | Value | Why |
| --- | --- | --- |
| `LIGHTFAST_DESKTOP_AGENT_MODE` | `1` | Skips `shell.openExternal` (Dia never opens) and emits structured stdout JSON instead. Without this, the flow opens the user's default browser and the agent has no way to read the URL. |
| `AGENT_BROWSER_HEADED` | `true` | **Mandatory.** Headless Chrome for Testing silently drops `lightfast-dev://` navigations — no prompt, no error, no fallback browser hand-off. The desktop's `app.on('open-url')` never fires and the agent times out with no diagnostic signal. Validated 2026-04-25 spike. |
| `LIGHTFAST_DESKTOP_AUTH_TIMEOUT_MS` | `30000` (recommended) | Default is 5 minutes for human users; agents want fast CI feedback. |

## Stdout event grammar

Desktop emits one JSON object per line on stdout (only when AGENT_MODE=1):

| Event | When | Payload |
| --- | --- | --- |
| `auth_already_signed_in` | App start, token already persisted | `{}` |
| `auth_signin_url` | App start, token absent — sign-in begun | `{ url: string }` |
| `auth_signed_in` | Exchange succeeded, token persisted | `{}` |
| `auth_signin_failed` | Timeout / exchange 4xx / state mismatch / persist failed | `{ reason: string }` |

Every `auth_signin_url` is followed by exactly one terminal event
(`auth_signed_in` OR `auth_signin_failed`) per in-flight sign-in.

## The flow

```sh
# 1. Start desktop in agent mode. Auto-triggers sign-in on app-ready when
# no token is persisted; idempotent if already signed in.
LIGHTFAST_DESKTOP_AGENT_MODE=1 \
LIGHTFAST_DESKTOP_AUTH_TIMEOUT_MS=30000 \
pnpm --filter @lightfast/desktop dev > /tmp/desktop.log 2>&1 &

# 2. Read the first lifecycle event (auth_already_signed_in OR auth_signin_url).
EVENT=$(timeout 30 sh -c "tail -F /tmp/desktop.log | jq -rcM --unbuffered 'select(.event)' | head -1")
case "$(echo "$EVENT" | jq -r .event)" in
auth_already_signed_in) echo "Already signed in"; exit 0 ;;
auth_signin_url) SIGNIN_URL=$(echo "$EVENT" | jq -r .url) ;;
*) echo "Unexpected event: $EVENT"; exit 1 ;;
esac

# 3. Headed agent-browser navigates to the URL. Clerk completes, browser
# dispatches lightfast-dev://auth/callback?code=…&state=…, OS routes to
# the running desktop, exchange runs, token persists.
AGENT_BROWSER_HEADED=true agent-browser open "$SIGNIN_URL"

# 4. Block on completion event.
RESULT=$(timeout 30 sh -c "tail -F /tmp/desktop.log | jq -rcM --unbuffered 'select(.event==\"auth_signed_in\" or .event==\"auth_signin_failed\")' | head -1")
echo "$RESULT" | jq -e '.event=="auth_signed_in"' > /dev/null
```

No CDP attach to the renderer, no log-grep — just JSON parse off stdout. Verify
with `pgrep -l Dia` before/after that no Dia process was spawned.

## Failure modes

| Symptom | Most likely cause |
| --- | --- |
| `auth_signin_failed{reason:"timeout"}` | **Forgot `AGENT_BROWSER_HEADED=true`.** Headless Chromium dropped the `lightfast-dev://` navigation silently. This is the #1 cause. |
| `auth_signin_failed{reason:"exchange_failed"}` | API unreachable, or the code expired (30s TTL). Check `pnpm dev:full` is running and Upstash Redis env is configured. |
| `auth_signin_failed{reason:"persist_failed"}` | Electron `safeStorage` unavailable on this host (rare; usually macOS Keychain access denied). |
| `auth_signin_failed{reason:"handler_error"}` | Custom-scheme URL parsing or unexpected callback shape. Check the desktop log; surface to engineering. |
| No event at all within 30s | Desktop didn't start in agent mode, or `pnpm dev:full` mesh is down on `:3024`. Check the bootstrap line in stdout. |

## Hygiene

- `agent-browser close --all` between runs if you need a *fresh* sign-in.
Otherwise the daemon profile retains Clerk session cookies and the next run
will short-circuit through Clerk silently — fine if that's what you want.
- Sign-out: dispatch the existing IPC `auth:sign-out` from the renderer, or
delete `~/Library/Application Support/Lightfast Dev/auth.bin` (macOS).

## Refusal conditions

Refuse to run when:
- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` starts with `pk_live_`.
- `LIGHTFAST_API_URL` is set to anything other than a localhost URL.

These are the same guardrails as `lightfast-clerk` — this skill is dev-only.
13 changes: 13 additions & 0 deletions api/app/src/__tests__/resolve-clerk-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ describe("resolveClerkSession", () => {
expect(authMock).toHaveBeenCalledWith({ treatPendingAsSignedOut: false });
});

it("returns null when the Bearer JWT is invalid and no cookie session exists", async () => {
verifyTokenMock.mockRejectedValueOnce(new Error("jwt expired"));
authMock.mockResolvedValueOnce({ userId: null, orgId: null });

const session = await resolveClerkSession(
new Headers({ authorization: "Bearer expired.jwt" })
);

expect(session).toBeNull();
expect(verifyTokenMock).toHaveBeenCalledTimes(1);
expect(authMock).toHaveBeenCalledWith({ treatPendingAsSignedOut: false });
});

it("returns null when neither Bearer nor cookie produce a session", async () => {
authMock.mockResolvedValueOnce({ userId: null, orgId: null });

Expand Down
File renamed without changes.
4 changes: 3 additions & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,17 @@
"@repo/vitest-config": "workspace:*",
"@tailwindcss/postcss": "catalog:tailwind4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@types/lodash.merge": "^4.6.9",
"@types/node": "catalog:",
"@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19",
"@vitejs/plugin-react": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/expect": "catalog:",
"babel-plugin-react-compiler": "catalog:",
"dotenv-cli": "catalog:",
"happy-dom": "^20.9.0",
"happy-dom": "catalog:",
"import-in-the-middle": "catalog:",
"postcss": "catalog:tailwind4",
"require-in-the-middle": "catalog:",
Expand Down
Loading
Loading