Code editor, VCS & GitHub
Description
An in-browser IDE over a group's linked repository, with a review workflow layered on top. A member edits files, commits to a branch, and raises a change request; the group owner reviews the diff and merges or rejects. Changes reach GitHub only through that gate.
Includes a Groq-backed AI assistant for explaining and editing code, and README generation.
Setup
# GitHub OAuth with `repo` scope
GITHUB_ID=...
GITHUB_SECRET=...
# Encrypts stored access tokens — 64 hex chars
ENCRYPTION_KEY=...
# Git microservice
GIT_SERVICE_URL=http://localhost:8080
GIT_SERVICE_SECRET=...
# AI assistant (optional)
GROQ_API_KEY=gsk_...
GROQ_MODEL=llama-3.3-70b-versatile
The WebSocket service must be running — it hosts the git microservice that reads repository files.
Then, in the app: link a repo at /github, and grant members code access.
Integration
Frontend Backend Git service
──────── ─────── ───────────
/code-editor/[g]/[repo]
├─ file tree ───────────► GET /api/files ──────────► POST /git/content
├─ open file ──────────► GET /api/file-content ───► GET /git/file
├─ edit ────────────────► draftStore (S3)
├─ commit ─────────────► POST /api/commit-changes
└─ raise CR ───────────► POST /api/vcs/change-request
│
/confirm-changes/[g] ▼
├─ diff ───────────────► GET /api/modified-files
├─ approve ────────────► POST /api/vcs/merge ──────► GitHub
└─ reject ─────────────► POST /api/vcs/reject
Code access
CodeAccessStatus is a four-state machine on GroupMember:
enum CodeAccessStatus {
NONE // no repository access
PENDING_GITHUB // awaiting a GitHub collaborator invite
INVITED // invite sent, not yet accepted
ACTIVE // full access
}
Only ACTIVE members can commit. The transitions matter: an owner grants
access, Ko-Lab sends a GitHub collaborator invitation via
lib/githubCollaborator.ts, and the member becomes ACTIVE only once
GitHub confirms acceptance.
:::danger This state machine has no tests It gates writes to a real repository and is entirely uncovered. See Testing. :::
Change requests
enum ChangeRequestStatus {
OPEN
MERGED
REJECTED
CONFLICT
}
CONFLICT is set when the base SHA has moved since the CR was raised —
/api/vcs/base-sha captures it at creation, and the merge path compares
before applying.
How it works
Tokens are encrypted at rest
GitHub access tokens are stored encrypted with AES-256-CBC
(apps/web/app/lib/encryption.ts), keyed by ENCRYPTION_KEY:
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
Rotating that key invalidates every stored token and forces all users to reconnect GitHub.
:::danger Hardcoded fallback key
encryption.ts:5-6 falls back to a key committed in the repository when
ENCRYPTION_KEY is unset. Tokens encrypted under it are effectively
plaintext. Always set the variable explicitly.
:::
Git operations are serialised per group
Clones live at workspaces/<groupId>/<branch>/. Concurrent operations on one
working directory corrupt it, so every mutating path goes through
withGroupLock (apps/web-socket/src/gitWorkspace.ts:11).
Path traversal is guarded
safeResolve (gitWorkspace.ts:26) resolves a requested path and rejects
anything escaping the workspace root. This is the only thing standing between
a crafted path parameter and the host filesystem — see the
traversal test.
Drafts
Unsaved edits go to DRAFT_BUCKET via lib/draftStore.ts, so a reload does
not lose work. Drafts are per user and per file, and are cleared on commit.
Testing
Automated
None. This is the largest untested surface in the codebase. Nothing covers code access, change requests, merges, or GitHub integration.
Three tests worth writing first, in priority order:
1. The codeAccess gate — it protects repository writes:
describe("POST /api/commit-changes", () => {
it.each(["NONE", "PENDING_GITHUB", "INVITED"])(
"rejects a commit from a member with codeAccess=%s",
async (status) => {
vi.mocked(prisma.groupMember.findFirst).mockResolvedValue({
userId: "u2", groupId: "g1", codeAccess: status,
} as never);
const res = await POST(commitRequest({ groupId: "g1", userId: "u2" }));
expect(res.status).toBe(403);
},
);
it("allows a commit from an ACTIVE member", async () => { /* … */ });
});
2. Merge authorization — only the owner should merge:
it("rejects a merge by a non-owner", async () => {
const res = await POST(mergeRequest({ crId: "cr1", callerId: "notOwner" }));
expect(res.status).toBe(403);
});
3. Conflict detection — a moved base SHA must not merge silently:
it("marks the CR CONFLICT when the base SHA has moved", async () => {
// CR created at sha A; branch now at sha B
const res = await POST(mergeRequest({ crId: "cr1" }));
expect((await res.json()).status).toBe("CONFLICT");
});
Manual
Needs a real GitHub repo you can safely write to. Use a throwaway repo — these flows create branches and commits.
Linking
- Sign in with GitHub, open
/github - Link a repository to a group
/api/github/statusreports connected- Confirm the token is stored encrypted — inspect the row in Prisma Studio and verify it is not readable plaintext
Editor
- Open
/code-editor/<groupId>/<repo> - File tree loads — this exercises the git service
- Open a file → syntax highlighting matches the language
- Edit, reload the page → the draft survives
- Open a large file → check the chunked path, not a hang
- Open a binary file → handled gracefully, not rendered as text
Access control — the important one
- As owner, grant B code access → B becomes
PENDING_GITHUB, thenINVITED - B accepts the GitHub invitation →
ACTIVE - As a
NONEmember, attempt a commit → must fail with 403 - As
INVITED(not yet accepted), attempt a commit → must fail - Revoke access → the next commit fails
Steps 3–5 are the ones to re-run after any change near this code, since nothing automated protects it.
Change request flow
- B (ACTIVE) edits a file, commits to a branch
- B raises a change request
- Owner sees it at
/confirm-changes/<groupId> - The diff renders correctly
- Approve → verify the commit lands on GitHub
- Reject with a reason → B sees it in
/notificationsand/rejected-cr
Conflicts
- B raises a CR from base SHA A
- Push a separate commit to the same branch directly on GitHub
- Owner attempts the merge → status becomes
CONFLICT, nothing is silently overwritten
AI assistant
- Select code, ask the assistant to explain it
- Ask for an edit → the proposal is reviewable before applying
- Unset
GROQ_API_KEY→ the feature degrades with a clear message rather than crashing POST /api/generate-readmeproduces sensible output
Security checks
- Path traversal through the file endpoints — see WebSocket testing
- Request another group's
groupIdas a non-member → must be refused - Confirm no access token appears in any API response or client bundle
Demo
:::info Video coming soon Editing a file, raising a change request, and merging it to GitHub. :::