Skip to content

Commit a7b7c93

Browse files
committed
Merge origin/dev: resolve conflicts in SSO, model-selector, execution, ISSUES.md
Keep HMAC-signed SSO state (HEAD) over insecure dev version. Accept dev versions for unrelated model-selector/execution changes. Merge both sides of ISSUES.md work log entries.
2 parents 7c94ba1 + 474430c commit a7b7c93

45 files changed

Lines changed: 1491 additions & 387 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.dev

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Database Configuration — Development branch
2+
DATABASE_URL=postgresql://neondb_owner:npg_FVijz3A5Srpl@ep-lively-tooth-aot5ofmy-pooler.c-2.ap-southeast-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
3+
# GitHub OAuth App
4+
GITHUB_CLIENT_ID=Ov23liy8A1JNx3VSY0la
5+
GITHUB_CLIENT_SECRET=2331aaed22c6ad6260d0ebde3f60ad6ac4af29fe
6+
GITHUB_REDIRECT_URI=http://localhost:3001/api/auth/github/callback
7+
# Optional: personal access token for higher rate limits on public repo fetches
8+
GITHUB_TOKEN=
9+
# JWT signing secret. REQUIRED in production.
10+
JWT_SECRET=openlinear-dev-secret-change-in-production
11+
# Token encryption key (AES-256-GCM, 32 bytes base64). REQUIRED — encrypts GitHub tokens at rest.
12+
TOKEN_ENCRYPTION_KEY=rl51+Xu6VEpVErimC3dj6TZ4h90HX3553fsGQKaEMBs=
13+
# Frontend URL (web flow OAuth redirect; ignored for ?client=desktop callbacks)
14+
FRONTEND_URL=http://localhost:3000
15+
# Cloud API base URL baked into the desktop UI build. In dev we point at the local API
16+
# so the Tauri "Sign in with GitHub" button opens the local OAuth endpoint instead of
17+
# the (currently unhosted) https://openlinear.tech/api/* endpoints.
18+
NEXT_PUBLIC_CLOUD_API_URL=http://localhost:3001
19+
# CORS origin. Comma-separated list. Tauri origins (tauri://localhost, https://tauri.localhost) are always added implicitly.
20+
CORS_ORIGIN=http://localhost:3000
21+
# API server port
22+
API_PORT=3001
23+
# OAuth interceptor port used by the local sidecar
24+
OAUTH_INTERCEPTOR_PORT=1455
25+
# Optional: where the sidecar clones repos into. Defaults to <tmp>/openlinear-repos.
26+
REPOS_DIR=
27+
# Brainstorm AI Configuration
28+
BRAINSTORM_API_KEY=
29+
BRAINSTORM_MODEL=gpt-4o-mini
30+
BRAINSTORM_PROVIDER=openai
31+
BRAINSTORM_BASE_URL=

.env.prod

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.env.production — Production (61 lines)
2+
# Production Environment
3+
# Copy this file to `.env` on your production server or use your deployment platform's secret management.
4+
# Production Database
5+
# Branch: br-patient-art-aocs682x (production, primary)
6+
# Endpoint: ep-wild-feather-aolgvb3d-pooler
7+
DATABASE_URL=postgresql://neondb_owner:npg_FVijz3A5Srpl@ep-wild-feather-aolgvb3d-pooler.c-2.ap-southeast-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
8+
# GitHub OAuth App (same app handles both web and desktop via ?client=desktop)
9+
# Homepage URL: https://openlinear.tech
10+
# Callback URL: https://openlinear.tech/api/auth/github/callback (web + desktop deep-link)
11+
GITHUB_CLIENT_ID=Ov23limWISAAOhkqDFXX
12+
GITHUB_CLIENT_SECRET=6f41d6806ac0c79301f5a47c9c61279a50dbaee1
13+
GITHUB_REDIRECT_URI=https://openlinear.tech/api/auth/github/callback
14+
# Optional: GitHub PAT for higher rate limits on public repo fetches
15+
GITHUB_TOKEN=
16+
# JWT signing secret. REQUIRED — use a strong random string (e.g. `openssl rand -base64 32`).
17+
JWT_SECRET=
18+
# Token encryption key (AES-256-GCM, 32 bytes base64). REQUIRED — encrypts GitHub tokens at rest.
19+
# Generate with: openssl rand -base64 32
20+
TOKEN_ENCRYPTION_KEY=
21+
# Frontend URL
22+
FRONTEND_URL=https://openlinear.tech
23+
# Cloud API base URL baked into the desktop UI build
24+
NEXT_PUBLIC_CLOUD_API_URL=https://openlinear.tech
25+
# CORS origin. Tauri origins (tauri://localhost, https://tauri.localhost) are always added implicitly.
26+
CORS_ORIGIN=https://openlinear.tech
27+
# API server port
28+
API_PORT=3001
29+
# OAuth interceptor port used by the local sidecar
30+
OAUTH_INTERCEPTOR_PORT=1455
31+
# Optional: where the sidecar clones repos into
32+
REPOS_DIR=
33+
# Brainstorm AI Configuration
34+
BRAINSTORM_API_KEY=
35+
BRAINSTORM_MODEL=gpt-4o-mini
36+
BRAINSTORM_PROVIDER=openai
37+
BRAINSTORM_BASE_URL=
38+
# Home Chat LLM Configuration
39+
CHAT_LLM_BASE_URL=https://api.fireworks.ai/inference/v1
40+
CHAT_LLM_API_KEY=
41+
CHAT_LLM_MODEL=accounts/fireworks/models/kimi-k2p6
42+
CHAT_LLM_TIMEOUT_MS=60000
43+
CHAT_RATE_LIMIT_PER_MIN=60
44+
# ElevenLabs Speech-to-Text
45+
ELEVENLABS_API_KEY=
46+
ELEVENLABS_STT_MODEL_ID=scribe_v2
47+
ELEVENLABS_STT_TIMEOUT_MS=30000
48+
ELEVENLABS_STT_MAX_UPLOAD_MB=25

ISSUES.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,66 @@
55
66
---
77

8+
## [2026-06-02] — Fix SSO CSRF, session security, and upload auth (Greptile review)
9+
10+
**Status:** Done
11+
**Agent:** Sisyphus (OpenCode)
12+
13+
### What was done
14+
- Implemented all 8 findings from Greptile review c29aea46
15+
- **P0:** SSO state now HMAC-signed (prevents CSRF), OIDC `expectedState` properly passed instead of `undefined` cast
16+
- **P1:** SSO login creates Session records (tokens now revocable), revoke-others derives session ID from JWT hash, 2FA tempToken delivered via URL fragment, /uploads requires auth
17+
- **P2:** TOTP validate endpoint rate-limited (10 req/min/IP)
18+
19+
### Files changed
20+
- `apps/api/src/routes/sso.ts` — HMAC sign/verify for SSO state, createSession on login
21+
- `apps/api/src/services/sso.ts` — expectedState parameter to authorizationCodeGrant
22+
- `apps/api/src/routes/sessions.ts` — derive currentSessionId from JWT hash
23+
- `apps/api/src/routes/auth.ts` — tempToken via URL fragment
24+
- `apps/api/src/app.ts` — requireAuth on /uploads static serving
25+
- `apps/api/src/routes/totp.ts` — express-rate-limit on /validate
26+
27+
---
28+
## [2026-05-31] — Fix critical migration drift + docs accuracy + sidecar logging
29+
30+
**Status:** Done
31+
**Agent:** Kiro
32+
33+
### What was done
34+
- **CRITICAL — Migration drift fixed.** The committed Prisma migrations were badly out of sync with `schema.prisma`: a fresh `db:migrate deploy` produced only 21 tables / 8 `users` columns, while `db:push` (and prod, patched out-of-band) had 30+ tables / 12 `users` columns. This broke the documented `db:migrate:deploy` deploy path and the API test suite (14/18 files failed at setup with `column totpEnabled does not exist`).
35+
- Generated reconciliation SQL via `prisma migrate diff` (migration-built DB → schema.prisma) and added migration `20260531000000_reconcile_schema_drift`. It adds the 10 missing tables (`task_assignees`, `sso_configs`, `roles`, `chat_attachments`, `mcp_tool_calls`, `invitations`, `comment_mentions`, `notification_preferences`, `audit_logs`, `sessions`), 3 enums (`invitation_statuses`, `permissions`, `task_assignee_roles`), 4 `users` columns (`totpSecret`, `totpEnabled`, `backupCodes`, `displayName`), `settings.taskDeletionMode`, and `roleId` FKs on workspace_members/team_members/project_access — all with indexes + FKs.
36+
- Made every statement **idempotent** (`IF NOT EXISTS`, guarded `DO $$ ... EXCEPTION WHEN duplicate_object` blocks) so it is safe against the already-reconciled production DB.
37+
- Added the missing `prisma/migrations/migration_lock.toml` (provider = postgresql) — without it, `prisma migrate diff --from-migrations` could not read the migrations dir.
38+
- **Verified:** fresh `migrate deploy` (all migrations incl. reconcile) → 31 tables, 12 user columns, and `prisma migrate diff` reports **"No difference detected"** vs schema. API test suite went from **14 failed/4 passed** to **18 passed / 149 tests / exit 0**.
39+
- **Docs accuracy.** Removed the false "100% test coverage on execution services" claim from `docs/CODEBASE_INDEX.md` (sidecar actually has 0 automated tests). Updated `docs/ARCHITECTURE.md` deployment section (DigitalOcean/PM2 → Azure Container Apps + ACR, current `deploy-azure.yml` pipeline) and auth section (added PAT + TOTP 2FA + SSO alongside GitHub OAuth). Softened the "GitHub OAuth is the only login method" line in `docs/features/api-reference.md`.
40+
- **Sidecar logging.** Confirmed the `console.*` → pino `logger` migration across all `apps/sidecar/src` services/routes (was already present in the working tree; verified 0 `console.` calls remain in non-test sidecar source, typecheck clean). This closes the last open guardrail from the F1 plan-compliance review (REJECT-4).
41+
42+
### Files changed
43+
- `packages/db/prisma/migrations/20260531000000_reconcile_schema_drift/migration.sql` — new reconciliation migration (idempotent)
44+
- `packages/db/prisma/migrations/migration_lock.toml` — new (was missing)
45+
- `docs/CODEBASE_INDEX.md` — removed false 100% coverage claims (2 spots)
46+
- `docs/ARCHITECTURE.md` — Azure deployment + CI/CD rewrite; auth section updated (PAT/2FA/SSO)
47+
- `docs/features/api-reference.md` — auth line corrected
48+
- `apps/sidecar/src/**` — console→logger (verified; bulk of this was pre-existing working-tree work)
49+
50+
### Verification
51+
- `prisma migrate deploy` on a fresh DB → 31 tables / 12 user cols; `migrate diff` vs schema → no difference
52+
- `pnpm --filter @openlinear/api test` → 18 files passed, 149 tests, exit 0 (was 14 failing)
53+
- `@openlinear/api`, `@openlinear/sidecar`, `@openlinear/db` typecheck → all exit 0
54+
- `db:generate` → clean
55+
56+
### Issues encountered / notes
57+
- Prod Neon DB already has all 31 tables + TOTP columns (someone ran `db:push` against it directly), but its `_prisma_migrations` history is the same incomplete set. The new idempotent reconcile migration is safe to mark-applied or run there; recommend `prisma migrate resolve --applied 20260531000000_reconcile_schema_drift` on prod so history matches without re-running DDL.
58+
- Both CI workflows (`deploy.yml` legacy/disabled, `deploy-azure.yml` active) use `db:push` for their ephemeral test DB, which is why CI didn't catch the drift. Consider switching CI to `migrate deploy` so the migration path is continuously exercised.
59+
- Pre-existing uncommitted working-tree changes (chat attachments, transcribe Cartesia URL, opencode init-guard, etc.) were left untouched per protocol.
60+
61+
### Next steps / blockers (follow-ups — NOT done this session)
62+
- **Sidecar/execution-core test coverage: 0 tests.** The execution engine (clones repos, force-pushes branches, creates PRs) and `packages/execution-core` have no automated tests. Highest-value remaining gap. Add Vitest coverage for lifecycle, git, worktree, batch orchestrator/completion, and the pure `event-stream-processor`.
63+
- **Multi-tenant OpenCode isolation** (`docs/limitations.md`): cloud deployment still shares one OpenCode process/auth store across users, gated only by `OPENLINEAR_ALLOW_SHARED_OPENCODE=1`. Real per-user isolation (per-user `XDG_DATA_HOME` + process pool) still pending.
64+
- Consider making CI run `prisma migrate deploy` against a fresh DB (in addition to / instead of `db:push`) to prevent future drift.
65+
66+
---
67+
868
## [2026-05-26] — Fix sidecar startup crash (missing openid-client + otpauth + ALLOW_SHARED_OPENCODE)
969

1070
**Status:** Done
@@ -1583,3 +1643,23 @@ The sidecar was likely not running when login was attempted (single-tenant guard
15831643

15841644
### Next steps / blockers
15851645
- None for the execution workflow and Activity tab fixes.
1646+
## [2026-05-27] — Full codebase review, index, and CODEBASE_INDEX.md
1647+
1648+
**Status:** Done
1649+
**Agent:** Sisyphus (OpenCode)
1650+
1651+
### What was done
1652+
- Ran 4 parallel `kiroExplore` background agents exploring API, Frontend, Sidecar, and MCP/DB layers
1653+
- Read ~40+ key source files directly across all layers
1654+
- Cross-referenced results with direct reads to verify accuracy
1655+
- Created comprehensive `docs/CODEBASE_INDEX.md` (651 lines, 12 sections)
1656+
1657+
### Files changed
1658+
- `docs/CODEBASE_INDEX.md` — New. Complete index covering: architecture overview, API (28 routes, 5 middleware, 12 services, 34 chat tools, auth, error handling, rate limiting, tests), Sidecar execution engine (17 execution files, 5 batch files, safeguards, execution flow), Frontend (15 pages, 27 component dirs, state management, API client, SSE, keyboard shortcuts), Desktop Tauri (4 Rust files, sidecar manager, deeplink), MCP Server (8 tools, stateless Worker, API integration), Database (25+ models, 13 enums, Prisma patterns, migrations), shared packages, CI/CD, patterns/conventions, production URLs, and key commands
1659+
1660+
### Issues encountered
1661+
- 3 of 4 background sessions returned no output (likely expired); recovered by reading files directly
1662+
- Background sessions were "idle" but had full results in the final turn — retrieve messages worked on resumption
1663+
1664+
### Next steps / blockers
1665+
- None. Codebase fully indexed.

apps/api/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ function isGithubOAuthPath(path: string): boolean {
119119
export function createApp(): Application {
120120
const app: Application = express();
121121

122+
app.set('trust proxy', 1);
123+
122124
// Security headers — CSP disabled because Tauri owns CSP in the desktop UI
123125
app.use(
124126
helmet({

apps/api/src/lib/chat-prompts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export function buildChatSystemPrompt(input: BuildSystemPromptInput): string {
1515
'When a tool returns a permission error, explain the permission boundary plainly and do not suggest bypasses.',
1616
'Prefer deterministic identifiers from tool results over fuzzy names. If a user names something ambiguously, call search first.',
1717
'For combined setup requests that mention many issues/tasks plus labels, label colors, or a deadline, use setup_project_plan in one call instead of separate create_label/update_project/bulk_create_issues calls.',
18-
'When the user explicitly asks to create/add/done, create the requested records; use dryRun=true only when the user asks for a preview or brainstorm.',
18+
'When the user explicitly asks to create/add/done, create the requested records; use dryRun=true only when the user literally says "preview" or "dry run".',
1919
'Never say records were created, updated, moved, or archived until the corresponding mutating tool has returned ok=true.',
20-
'For standalone bulk issue planning, use bulk_create_issues with dryRun=true first unless the user explicitly asks to create/add now.',
20+
'For standalone bulk issue planning, use bulk_create_issues with dryRun=false to create immediately. Only use dryRun=true when the user literally asks for a preview.',
2121
'For changing many existing issues at once, use bulk_update_issues. Map user words like completed/complete to the done status.',
2222
'For deleting/removing/clearing existing active issues in a selected project, use archive_issues. Omit issue ids only when the user clearly means all active issues in the project.',
2323
'Keep final answers short, source-grounded, and action-oriented. Mention the exact records changed when tools mutate data.',

apps/api/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const PRISMA_TX_OPTIONS = { timeout: 15_000, maxWait: 5_000 } as const;

apps/api/src/routes/activity-log.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
assertTeamRole,
99
} from '../services/ownership';
1010
import { getUserTeamIds } from '../services/team-scope';
11+
import { PRISMA_TX_OPTIONS } from '../lib/constants';
1112
import { paginated, paginationSkipTake } from '../schemas/pagination';
1213
import { ValidationError } from '../errors';
1314

@@ -71,7 +72,7 @@ router.get('/', requireAuth, async (req: AuthRequest, res: Response) => {
7172
const count = await tx.activityLog.count({ where });
7273
return [items, count] as const;
7374
},
74-
{ timeout: 15000, maxWait: 5000 },
75+
PRISMA_TX_OPTIONS,
7576
);
7677

7778
res.json(paginated(activities, total, page, pageSize));

apps/api/src/routes/agent-runs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { assertTaskAccess } from '../services/ownership';
66
import { getUserTeamIds } from '../services/team-scope';
77
import { paginated, paginationSkipTake } from '../schemas/pagination';
88
import { HttpError, ValidationError } from '../errors';
9+
import { PRISMA_TX_OPTIONS } from '../lib/constants';
910

1011
const router: Router = Router();
1112

@@ -75,7 +76,7 @@ router.get(
7576
const count = await tx.agentRun.count({ where });
7677
return [items, count] as const;
7778
},
78-
{ timeout: 15000, maxWait: 5000 },
79+
PRISMA_TX_OPTIONS,
7980
);
8081

8182
res.json(paginated(runs, total, page, pageSize));

apps/api/src/routes/dashboard.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ router.get(
1717
? { OR: [{ creatorId: userId }, { assigneeId: userId }, { teamId: { in: teamIds } }] }
1818
: { OR: [{ creatorId: userId }, { assigneeId: userId }] };
1919

20-
const [statusCounts, priorityCounts, overdue, unassigned, total, recent] = await Promise.all([
20+
const results = await Promise.allSettled([
2121
prisma.task.groupBy({
2222
by: ['status'],
2323
where: { ...where, archived: false },
@@ -54,6 +54,13 @@ router.get(
5454
}),
5555
]);
5656

57+
const statusCounts = results[0].status === 'fulfilled' ? results[0].value : [];
58+
const priorityCounts = results[1].status === 'fulfilled' ? results[1].value : [];
59+
const overdue = results[2].status === 'fulfilled' ? results[2].value : 0;
60+
const unassigned = results[3].status === 'fulfilled' ? results[3].value : 0;
61+
const total = results[4].status === 'fulfilled' ? results[4].value : 0;
62+
const recent = results[5].status === 'fulfilled' ? results[5].value : 0;
63+
5764
const statusMap = Object.fromEntries(statusCounts.map((s) => [s.status, s._count._all]));
5865
const priorityMap = Object.fromEntries(priorityCounts.map((p) => [p.priority, p._count._all]));
5966

apps/api/src/routes/inbox.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { validateQuery, ValidatedRequest } from '../middleware/validate';
55
import { getUserTeamIds } from '../services/team-scope';
66
import { assertTaskOwned } from '../services/ownership';
77
import { paginationQuerySchema, paginated, paginationSkipTake, PaginationQuery } from '../schemas/pagination';
8+
import { PRISMA_TX_OPTIONS } from '../lib/constants';
89

910
const router: Router = Router();
1011

@@ -70,7 +71,7 @@ router.get(
7071
const count = await tx.task.count({ where });
7172
return [items, count] as const;
7273
},
73-
{ timeout: 15000, maxWait: 5000 },
74+
PRISMA_TX_OPTIONS,
7475
);
7576

7677
const flatTasks = tasks.map(task => ({

0 commit comments

Comments
 (0)