@@ -15,23 +15,23 @@ Tiny API that clones a Git repo and counts its non-empty lines of code.
|
|||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
- `GET /loc.txt?repo=<git-url>&ssh_key=<optional-key-file>&ref=<optional-ref>`
|
- `GET /loc.txt?repo=<git-url>&ssh_key=<optional-key-file>&ref=<optional-ref>&api_key=<optional-api-key>`
|
||||||
- Returns the line count as plain text.
|
- Returns the line count as plain text.
|
||||||
- `GET /loc?repo=<git-url>&ssh_key=<optional-key-file>&ref=<optional-ref>`
|
- `GET /loc?repo=<git-url>&ssh_key=<optional-key-file>&ref=<optional-ref>&api_key=<optional-api-key>`
|
||||||
- Returns JSON metadata, including a language breakdown by files and non-empty lines.
|
- Returns JSON metadata, including a language breakdown by files and non-empty lines.
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
- Health plus queue/cache stats.
|
- Health plus queue/cache stats.
|
||||||
|
|
||||||
You can also pass `query=<git-url>` as a compatibility alias for `repo`.
|
|
||||||
This is considered unsafe and discouraged because the name is too generic and easier to misuse or collide with upstream tooling.
|
|
||||||
Prefer `repo` unless you explicitly need legacy compatibility.
|
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
No auth by default. If `API_KEY` is set, send it as either:
|
No auth by default. If `API_KEY` is set, send it as either:
|
||||||
|
|
||||||
- `x-api-key: ...`
|
- `x-api-key: ...`
|
||||||
- `Authorization: Bearer ...`
|
- `Authorization: Bearer ...`
|
||||||
|
- `api_key=...` query param
|
||||||
|
|
||||||
|
The `api_key` query param is unsafe and discouraged because it can leak through logs, browser history, analytics, caches, and referrers.
|
||||||
|
Prefer `x-api-key` or `Authorization` whenever possible.
|
||||||
|
|
||||||
## SSH keys
|
## SSH keys
|
||||||
|
|
||||||
|
|||||||
+3
-6
@@ -42,8 +42,9 @@ export function createApp(config: RuntimeConfig, deps: AppDependencies) {
|
|||||||
|
|
||||||
const headerKey = req.header("x-api-key");
|
const headerKey = req.header("x-api-key");
|
||||||
const bearer = req.header("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
const bearer = req.header("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
||||||
|
const queryKey = readOptionalString(req.query.api_key);
|
||||||
|
|
||||||
if (headerKey === config.apiKey || bearer === config.apiKey) {
|
if (headerKey === config.apiKey || bearer === config.apiKey || queryKey === config.apiKey) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -71,11 +72,7 @@ export function createApp(config: RuntimeConfig, deps: AppDependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readCountRequest(req: Request): CountRequest {
|
function readCountRequest(req: Request): CountRequest {
|
||||||
const repo = typeof req.query.repo === "string"
|
const repo = typeof req.query.repo === "string" ? req.query.repo.trim() : "";
|
||||||
? req.query.repo.trim()
|
|
||||||
: typeof req.query.query === "string"
|
|
||||||
? req.query.query.trim()
|
|
||||||
: "";
|
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new HttpError(400, "Missing repo query parameter");
|
throw new HttpError(400, "Missing repo query parameter");
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-24
@@ -45,30 +45,6 @@ test("GET /loc returns count metadata", async () => {
|
|||||||
assert.equal(response.body.languages[0].language, "TypeScript");
|
assert.equal(response.body.languages[0].language, "TypeScript");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("GET /loc accepts query as a repo alias", async () => {
|
|
||||||
const app = createApp(baseConfig, {
|
|
||||||
getHealth: () => ({ cacheEntries: 0, inFlight: 0, maxConcurrentScans: 4, activeScans: 0, queuedScans: 0 }),
|
|
||||||
getPublicKey: async () => "ssh-ed25519 AAAA",
|
|
||||||
getDefaultKeyName: () => "loc_via_git_ed25519",
|
|
||||||
count: async (requestInput) => ({
|
|
||||||
repo: requestInput.repo,
|
|
||||||
ref: requestInput.ref,
|
|
||||||
sshKey: requestInput.sshKey,
|
|
||||||
cached: false,
|
|
||||||
lineCount: 2,
|
|
||||||
fileCount: 1,
|
|
||||||
languages: [{ language: "Plain Text", files: 1, lines: 2 }],
|
|
||||||
scannedAt: "2026-01-01T00:00:00.000Z",
|
|
||||||
durationMs: 10
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await request(app).get("/loc?query=https://example.com/query.git");
|
|
||||||
|
|
||||||
assert.equal(response.status, 200);
|
|
||||||
assert.equal(response.body.repo, "https://example.com/query.git");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("GET /ssh/public-key returns text without auth", async () => {
|
test("GET /ssh/public-key returns text without auth", async () => {
|
||||||
const app = createApp({ ...baseConfig, apiKey: "secret" }, {
|
const app = createApp({ ...baseConfig, apiKey: "secret" }, {
|
||||||
getHealth: () => ({ cacheEntries: 0, inFlight: 0, maxConcurrentScans: 4, activeScans: 0, queuedScans: 0 }),
|
getHealth: () => ({ cacheEntries: 0, inFlight: 0, maxConcurrentScans: 4, activeScans: 0, queuedScans: 0 }),
|
||||||
@@ -99,3 +75,27 @@ test("GET /loc enforces api key when configured", async () => {
|
|||||||
|
|
||||||
assert.equal(response.status, 401);
|
assert.equal(response.status, 401);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /loc accepts api_key query param as an unsafe fallback", async () => {
|
||||||
|
const app = createApp({ ...baseConfig, apiKey: "secret" }, {
|
||||||
|
getHealth: () => ({ cacheEntries: 0, inFlight: 0, maxConcurrentScans: 4, activeScans: 0, queuedScans: 0 }),
|
||||||
|
getPublicKey: async () => "ssh-ed25519 AAAA",
|
||||||
|
getDefaultKeyName: () => "loc_via_git_ed25519",
|
||||||
|
count: async (requestInput) => ({
|
||||||
|
repo: requestInput.repo,
|
||||||
|
ref: requestInput.ref,
|
||||||
|
sshKey: requestInput.sshKey,
|
||||||
|
cached: false,
|
||||||
|
lineCount: 5,
|
||||||
|
fileCount: 1,
|
||||||
|
languages: [{ language: "Plain Text", files: 1, lines: 5 }],
|
||||||
|
scannedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
durationMs: 10
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app).get("/loc?repo=https://example.com/repo.git&api_key=secret");
|
||||||
|
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert.equal(response.body.lineCount, 5);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user