default scanned repo to private
MREADME.md
| @@ -80,6 +80,7 @@ All settings are environment variables: | |||
|---|---|---|---|
| 80 | 80 | | `MAX_USER_UPLOAD_BYTES` | `2097152` | Max upload size for non-admin users (2 MB) | | |
| 81 | 81 | | `INLINE_MAX_BYTES` | `524288` | Max file size rendered inline in the code view (512 KB) | | |
| 82 | 82 | | `SSH_DISABLED` | `0` | Set to `1` to disable the embedded SSH server | | |
| 83 | + | | `SCANNED_REPO_PRIVATE` | `1` | Set to `0` to make auto-scanned repos public by default | | |
| 83 | 84 | | `TRUSTED_PROXY` | `0` | Trust `X-Forwarded-For` headers \*\* | | |
| 84 | 85 | | `RATE_LIMIT_DISABLED` | `0` | Set to `1` to disable rate limiting | | |
| 85 | 86 | | `HIGHLIGHT_WORKERS` | `4` | Syntax highlighting worker threads \* | | |
Mcompose.yml
| @@ -15,6 +15,7 @@ services: | |||
|---|---|---|---|
| 15 | 15 | BASE_URL: "http://localhost:3000" | |
| 16 | 16 | # ADMIN_PASSWORD: change-me | |
| 17 | 17 | # SSH_DISABLED: 0 | |
| 18 | + | # SCANNED_REPO_PRIVATE: 1 | |
| 18 | 19 | # ARCHIVE_ZST_ENABLED: 0 | |
| 19 | 20 | # TRUSTED_PROXY: 1 | |
| 20 | 21 | # REGISTRATION_TYPE: enabled | |
Msrc/config.ts
| @@ -19,6 +19,9 @@ const config = { | |||
|---|---|---|---|
| 19 | 19 | TRUSTED_PROXY: !!env.TRUSTED_PROXY, | |
| 20 | 20 | RATE_LIMIT_DISABLED: !!env.RATE_LIMIT_DISABLED, | |
| 21 | 21 | SSH_DISABLED: !!env.SSH_DISABLED, | |
| 22 | + | SCANNED_REPO_PRIVATE: | |
| 23 | + | env.SCANNED_REPO_PRIVATE !== "0" && | |
| 24 | + | env.SCANNED_REPO_PRIVATE !== "false", | |
| 22 | 25 | PORT: parseInt(env.PORT ?? "", 10) || 3000, | |
| 23 | 26 | SSH_PORT: parseInt(env.SSH_PORT ?? "", 10) || 2222, | |
| 24 | 27 | REGISTRATION_TYPE: (env.REGISTRATION_TYPE ?? "enabled") as | |
Msrc/index.tsx
| @@ -10,6 +10,6 @@ for (const cmd of ["git", "ssh-keygen"]) { | |||
|---|---|---|---|
| 10 | 10 | } | |
| 11 | 11 | } | |
| 12 | 12 | ||
| 13 | - | if (!config.SSH_DISABLED) await startSshServer(); | |
| 14 | 13 | await createApp(config.PORT); | |
| 14 | + | if (!config.SSH_DISABLED) await startSshServer(); | |
| 15 | 15 | console.log(`Hearthforge running at http://localhost:${config.PORT}`); | |
Msrc/services/repoSync.ts
| @@ -103,7 +103,7 @@ export async function ensureRepoRecord(name: string): Promise<RepositoryRow> { | |||
|---|---|---|---|
| 103 | 103 | .values({ | |
| 104 | 104 | name, | |
| 105 | 105 | description: null, | |
| 106 | - | is_private: 0, | |
| 106 | + | is_private: config.SCANNED_REPO_PRIVATE ? 1 : 0, | |
| 107 | 107 | default_branch: branch, | |
| 108 | 108 | created_at: now, | |
| 109 | 109 | }) | |
Mtests/e2e.releases.test.ts
| @@ -351,6 +351,12 @@ describe('releases', () => { | |||
|---|---|---|---|
| 351 | 351 | }); | |
| 352 | 352 | ||
| 353 | 353 | test('source archive tar.zst appears in downloads', async () => { | |
| 354 | + | const zstd = Bun.spawnSync({ cmd: ['which', 'zstd'] }); | |
| 355 | + | if (zstd.exitCode !== 0) { | |
| 356 | + | console.log('zstd not available, skipping tar.zst test'); | |
| 357 | + | return; | |
| 358 | + | } | |
| 359 | + | ||
| 354 | 360 | const page = await adminCtx.newPage(); | |
| 355 | 361 | try { | |
| 356 | 362 | await page.goto(srcReleaseUrl); | |
Mtests/e2e.repos.test.ts
| @@ -1,9 +1,14 @@ | |||
|---|---|---|---|
| 1 | 1 | import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; | |
| 2 | + | import { rmSync } from 'node:fs'; | |
| 3 | + | import { spawnSync } from 'node:child_process'; | |
| 2 | 4 | import { chromium } from 'playwright'; | |
| 3 | 5 | import type { Browser, BrowserContext } from 'playwright'; | |
| 6 | + | import config from '../src/config.ts'; | |
| 7 | + | import { db } from '../src/db/index.ts'; | |
| 4 | 8 | import { | |
| 5 | 9 | BASE, | |
| 6 | 10 | ADMIN_PASS, | |
| 11 | + | DATA_DIR, | |
| 7 | 12 | setupTestEnv, | |
| 8 | 13 | spawnServer, | |
| 9 | 14 | killServer, | |
| @@ -340,4 +345,27 @@ describe('repos', () => { | |||
|---|---|---|---|
| 340 | 345 | expect(resp.status()).toBe(200); | |
| 341 | 346 | expect(await resp.text()).toContain('Invalid repository name'); | |
| 342 | 347 | }); | |
| 348 | + | ||
| 349 | + | test('auto-scanned repo is private by default', async () => { | |
| 350 | + | const name = 'auto-private-repo'; | |
| 351 | + | const dir = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`; | |
| 352 | + | ||
| 353 | + | rmSync(dir, { recursive: true, force: true }); | |
| 354 | + | const tmp = `/tmp/hf-scan-${Date.now()}`; | |
| 355 | + | try { | |
| 356 | + | spawnSync('git', ['init', '--bare', dir], { stdio: 'ignore' }); | |
| 357 | + | spawnSync('git', ['clone', dir, tmp], { stdio: 'ignore' }); | |
| 358 | + | spawnSync('git', ['-C', tmp, 'commit', '--allow-empty', '-m', 'init'], { stdio: 'ignore' }); | |
| 359 | + | spawnSync('git', ['-C', tmp, 'push', 'origin', 'HEAD:main'], { stdio: 'ignore' }); | |
| 360 | + | } finally { | |
| 361 | + | rmSync(tmp, { recursive: true, force: true }); | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | const { ensureRepoRecord } = await import('../src/services/repoSync.ts'); | |
| 365 | + | const repo = await ensureRepoRecord(name); | |
| 366 | + | expect(repo.is_private).toBe(1); | |
| 367 | + | ||
| 368 | + | await db.deleteFrom('repositories').where('name', '=', name).execute(); | |
| 369 | + | rmSync(dir, { recursive: true, force: true }); | |
| 370 | + | }); | |
| 343 | 371 | }); | |