-
Notifications
You must be signed in to change notification settings - Fork 0
chore: reconcile follow-up lanes #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' | ||
| import fs from 'fs/promises' | ||
| import path from 'path' | ||
|
|
||
| vi.mock('../../src/db/queries', () => ({ | ||
| listRepos: vi.fn(), | ||
| })) | ||
|
|
||
| import * as repoQueries from '../../src/db/queries' | ||
| import { proxyRequest } from '../../src/services/proxy' | ||
|
|
||
| const artifactsRoot = path.resolve(process.cwd(), 'test-artifacts/proxy') | ||
| const reposRoot = path.resolve(process.cwd(), 'workspace/repos') | ||
|
|
||
| describe('proxyRequest hardening', () => { | ||
| beforeEach(async () => { | ||
| vi.restoreAllMocks() | ||
| vi.mocked(repoQueries.listRepos).mockReturnValue([]) | ||
| await fs.rm(artifactsRoot, { recursive: true, force: true }) | ||
| await fs.rm(reposRoot, { recursive: true, force: true }) | ||
| await fs.mkdir(artifactsRoot, { recursive: true }) | ||
| await fs.mkdir(reposRoot, { recursive: true }) | ||
|
Comment on lines
+12
to
+22
|
||
| }) | ||
|
|
||
| afterEach(async () => { | ||
| await fs.rm(artifactsRoot, { recursive: true, force: true }) | ||
| await fs.rm(reposRoot, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| it('rejects proxy directory traversal outside the tracked repos base', async () => { | ||
| const outsidePath = path.join(artifactsRoot, 'outside') | ||
| await fs.mkdir(outsidePath, { recursive: true }) | ||
|
|
||
| const response = await proxyRequest( | ||
| new Request(`http://localhost/api/opencode/session?directory=${encodeURIComponent(outsidePath)}`), | ||
| {} as any, | ||
| ) | ||
|
|
||
| expect(response.status).toBe(403) | ||
| await expect(response.json()).resolves.toEqual({ error: 'Path traversal detected' }) | ||
| }) | ||
|
|
||
| it('rejects proxy requests for untracked repositories', async () => { | ||
| const untrackedRepo = path.join(reposRoot, 'untracked-repo') | ||
| await fs.mkdir(untrackedRepo, { recursive: true }) | ||
|
|
||
| const fetchSpy = vi.spyOn(globalThis, 'fetch') | ||
| const response = await proxyRequest( | ||
| new Request(`http://localhost/api/opencode/session?directory=${encodeURIComponent(untrackedRepo)}`), | ||
| {} as any, | ||
| ) | ||
|
|
||
| expect(response.status).toBe(403) | ||
| await expect(response.json()).resolves.toEqual({ error: 'Directory is not a tracked repository' }) | ||
| expect(fetchSpy).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('allows tracked repositories and preserves upstream JSON responses', async () => { | ||
| const trackedRepo = path.join(reposRoot, 'tracked-repo') | ||
| await fs.mkdir(trackedRepo, { recursive: true }) | ||
| vi.mocked(repoQueries.listRepos).mockReturnValue([ | ||
| { | ||
| id: 1, | ||
| localPath: 'tracked-repo', | ||
| fullPath: trackedRepo, | ||
| defaultBranch: 'main', | ||
| cloneStatus: 'ready', | ||
| clonedAt: Date.now(), | ||
| } as any, | ||
| ]) | ||
|
|
||
| const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({ ok: true }), { | ||
| status: 200, | ||
| headers: { | ||
| 'content-type': 'application/json', | ||
| connection: 'keep-alive', | ||
| }, | ||
| })) | ||
|
|
||
| const response = await proxyRequest( | ||
| new Request(`http://localhost/api/opencode/session?directory=${encodeURIComponent(trackedRepo)}`, { | ||
| headers: { | ||
| Authorization: 'Bearer secret-token', | ||
| Connection: 'keep-alive', | ||
| Host: 'localhost:3001', | ||
| 'X-Trace-Id': 'trace-123', | ||
| }, | ||
| }), | ||
| {} as any, | ||
| ) | ||
|
|
||
| expect(fetchSpy).toHaveBeenCalledWith('http://127.0.0.1:5551/session', { | ||
| method: 'GET', | ||
| headers: { | ||
| authorization: 'Bearer secret-token', | ||
| 'x-trace-id': 'trace-123', | ||
| }, | ||
| body: undefined, | ||
| }) | ||
| expect(response.status).toBe(200) | ||
| await expect(response.json()).resolves.toEqual({ ok: true }) | ||
| expect(response.headers.get('connection')).toBeNull() | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actions/setup-nodeis configured withcache: pnpmbefore pnpm is installed.setup-nodeexpectspnpmto already be on PATH when caching is enabled (it runspnpm store path), so this can fail the workflow. Install pnpm first (e.g.pnpm/action-setup@v5withrun_install: false), or removecache: pnpm/ switch to an explicit cache step after installing pnpm.