Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e44a4fc
chore: add pre-push hooks for security audit and type checking
concentrate-ai May 30, 2026
f0a09ff
fix: replace fabricated model IDs with real API slugs
concentrate-ai May 30, 2026
25011a2
feat: add Concentrate AI as an optional universal model router
concentrate-ai May 30, 2026
cc660b9
feat: ZDR-only Concentrate catalog + dynamic models in picker
concentrate-ai May 30, 2026
a07662c
feat: live model catalogs with ZDR-aware routing
concentrate-ai Jun 2, 2026
7cb8222
feat: lazy model catalog fetch + manual refresh in settings
concentrate-ai Jun 2, 2026
f9a8e22
fix: show correct model label before catalog loads
concentrate-ai Jun 2, 2026
309ddd3
fix: broaden OpenAI chat-capable model detection for gpt-5/o-series
concentrate-ai Jun 3, 2026
7df7fbd
fix: treat chat-latest aliases as OpenAI provider
concentrate-ai Jun 3, 2026
f2b23e6
fix: label chat-latest aliases as GPT-latest in model picker
concentrate-ai Jun 3, 2026
312afe8
fix: accept dynamic catalog model IDs in routing and UI fallbacks
concentrate-ai Jun 3, 2026
618cee2
chore: simplify .env.example — just add CONCENTRATE_API_KEY entry
concentrate-ai Jun 3, 2026
3ba2bf3
fix: restore gemini-3-flash-preview as default model to match upstream
concentrate-ai Jun 3, 2026
eadcb8c
fix: restore upstream OpenAI model constants (gpt-5.5, gpt-5.4-mini/n…
concentrate-ai Jun 3, 2026
42ad044
chore: add migration to extend user_api_keys provider constraint for …
concentrate-ai Jun 3, 2026
febb5f0
fix: restore Check icon for selected model in picker; restore label m…
concentrate-ai Jun 3, 2026
59fd0e0
fix: clear provider model cache when key state changes to avoid stale…
concentrate-ai Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions .githooks/audit-grace.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
#!/usr/bin/env node
"use strict";

const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const https = require("https");

const GRACE_HOURS = 24;
const CACHE_TTL_DAYS = 7;
const CACHE_FILE = path.join(__dirname, ".audit-cache");
const KNOWN_FILE = path.join(__dirname, "audit-known.json");
const BLOCK_SEVERITIES = new Set(["critical", "high"]);

const RED = "\x1b[31m";
const YELLOW = "\x1b[33m";
const GREEN = "\x1b[32m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";

function loadJson(filepath) {
try {
return JSON.parse(fs.readFileSync(filepath, "utf8"));
} catch {
return {};
}
}

function saveCache(cache) {
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
}

function fetchAdvisory(ghsaId) {
return new Promise((resolve, reject) => {
const options = {
hostname: "api.github.com",
path: `/advisories/${ghsaId}`,
headers: { "User-Agent": "mike-audit-hook/1.0" },
};
https
.get(options, (res) => {
let body = "";
res.on("data", (chunk) => (body += chunk));
res.on("end", () => {
if (res.statusCode !== 200) {
reject(new Error(`GitHub API ${res.statusCode} for ${ghsaId}`));
return;
}
try {
resolve(JSON.parse(body));
} catch (e) {
reject(e);
}
});
})
.on("error", reject);
});
}

function hoursAgo(isoDate) {
return (Date.now() - new Date(isoDate).getTime()) / (1000 * 60 * 60);
}

function isExpired(entry) {
if (!entry || !entry.expires) return false;
return new Date(entry.expires) < new Date();
}

async function run() {
const targetDir = process.argv[2];
if (!targetDir) {
console.error("Usage: audit-grace.cjs <directory>");
process.exit(1);
}

const absDir = path.resolve(targetDir);
const dirName = path.basename(absDir);

if (!fs.existsSync(path.join(absDir, "package.json"))) {
console.log(` ${DIM}skip ${dirName} (no package.json)${RESET}`);
process.exit(0);
}

let auditJson;
try {
const raw = execSync("npm audit --json 2>/dev/null", {
cwd: absDir,
encoding: "utf8",
maxBuffer: 10 * 1024 * 1024,
});
auditJson = JSON.parse(raw);
} catch (e) {
if (e.stdout) {
try {
auditJson = JSON.parse(e.stdout);
} catch {
console.error(` ${RED}failed to parse npm audit output for ${dirName}${RESET}`);
process.exit(1);
}
} else {
console.error(` ${RED}npm audit failed for ${dirName}${RESET}`);
process.exit(1);
}
}

const vulns = auditJson.vulnerabilities || {};
const known = loadJson(KNOWN_FILE);
const cache = loadJson(CACHE_FILE);
let cacheChanged = false;

const findings = [];

for (const [pkg, info] of Object.entries(vulns)) {
if (!BLOCK_SEVERITIES.has(info.severity)) continue;

const ghsaIds = [];
for (const v of info.via || []) {
if (typeof v === "object" && v.url) {
const match = v.url.match(/(GHSA-[a-z0-9-]+)/);
if (match) ghsaIds.push(match[1]);
}
}

if (ghsaIds.length === 0) {
findings.push({
pkg,
severity: info.severity,
ghsa: "unknown",
status: "BLOCKED",
reason: "No GHSA ID found — cannot verify age",
});
continue;
}

for (const ghsa of ghsaIds) {
const knownEntry = known[ghsa] || known[`pkg:${pkg}`];
if (knownEntry && !isExpired(knownEntry)) {
findings.push({
pkg,
severity: info.severity,
ghsa,
status: "ALLOWED",
reason: knownEntry.reason || "In allowlist",
});
continue;
}
if (knownEntry && isExpired(knownEntry)) {
findings.push({
pkg,
severity: info.severity,
ghsa,
status: "BLOCKED",
reason: `Allowlist entry expired ${knownEntry.expires}`,
});
continue;
}

let publishedAt = null;
const cached = cache[ghsa];
if (cached && hoursAgo(cached.fetched_at) < CACHE_TTL_DAYS * 24) {
publishedAt = cached.published_at;
} else {
try {
const advisory = await fetchAdvisory(ghsa);
publishedAt = advisory.published_at || advisory.created_at;
cache[ghsa] = { published_at: publishedAt, fetched_at: new Date().toISOString() };
cacheChanged = true;
} catch (err) {
findings.push({
pkg,
severity: info.severity,
ghsa,
status: "BLOCKED",
reason: `Cannot fetch advisory: ${err.message}`,
});
continue;
}
}

const ageHours = hoursAgo(publishedAt);
if (ageHours < GRACE_HOURS) {
findings.push({
pkg,
severity: info.severity,
ghsa,
status: "GRACE",
reason: `Published ${Math.round(ageHours)}h ago (< ${GRACE_HOURS}h grace)`,
});
} else {
findings.push({
pkg,
severity: info.severity,
ghsa,
status: "BLOCKED",
reason: `Published ${Math.round(ageHours / 24)}d ago`,
});
}
}
}

if (cacheChanged) saveCache(cache);

if (findings.length === 0) {
console.log(` ${GREEN}${dirName}: no critical/high vulnerabilities${RESET}`);
process.exit(0);
}

console.log(`\n ${dirName} audit findings:`);
console.log(` ${"─".repeat(76)}`);

for (const f of findings) {
const color = f.status === "BLOCKED" ? RED : f.status === "GRACE" ? YELLOW : GREEN;
const sev = f.severity.toUpperCase().padEnd(8);
const id = f.ghsa.padEnd(22);
const tag = `${color}${f.status.padEnd(7)}${RESET}`;
console.log(` ${sev} ${id} ${tag} ${DIM}${f.pkg}: ${f.reason}${RESET}`);
}
console.log(` ${"─".repeat(76)}\n`);

const hasBlocked = findings.some((f) => f.status === "BLOCKED");
const hasGrace = findings.some((f) => f.status === "GRACE");

if (hasBlocked) process.exit(1);
if (hasGrace) process.exit(2);
process.exit(0);
}

run().catch((err) => {
console.error(` ${RED}audit-grace error: ${err.message}${RESET}`);
process.exit(1);
});
30 changes: 30 additions & 0 deletions .githooks/audit-known.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"pkg:protobufjs": {
"package": "protobufjs",
"severity": "high",
"reason": "Transitive dep via @google/genai. No upstream fix available yet.",
"added": "2026-05-17",
"expires": "2026-08-17"
},
"pkg:@xmldom/xmldom": {
"package": "@xmldom/xmldom",
"severity": "high",
"reason": "Transitive dep via mammoth. No upstream fix available yet.",
"added": "2026-05-17",
"expires": "2026-08-17"
},
"pkg:fast-xml-builder": {
"package": "fast-xml-builder",
"severity": "high",
"reason": "Transitive dep via @aws-sdk. No upstream fix available yet.",
"added": "2026-05-17",
"expires": "2026-08-17"
},
"pkg:tmp": {
"package": "tmp",
"severity": "high",
"reason": "Transitive dep via libreoffice-convert. No upstream fix available yet.",
"added": "2026-05-17",
"expires": "2026-08-17"
}
}
84 changes: 84 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -uo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'

REPO_ROOT="$(git rev-parse --show-toplevel)"
HOOKS_DIR="$REPO_ROOT/.githooks"
PASS=0
FAIL=0
WARN=0
RESULTS=()

run_check() {
local name="$1"
shift
printf " ${DIM}running${RESET} %s ...\n" "$name"
local code=0
"$@" || code=$?
if [[ $code -eq 0 ]]; then
RESULTS+=("${GREEN}PASS${RESET} $name")
((PASS++))
elif [[ "$name" == *"audit"* && $code -eq 2 ]]; then
RESULTS+=("${YELLOW}WARN${RESET} $name ${DIM}(grace period)${RESET}")
((WARN++))
else
RESULTS+=("${RED}FAIL${RESET} $name")
((FAIL++))
fi
}

run_advisory() {
local name="$1"
shift
printf " ${DIM}running${RESET} %s ...\n" "$name"
local code=0
"$@" || code=$?
if [[ $code -eq 0 ]]; then
RESULTS+=("${GREEN}PASS${RESET} $name")
((PASS++))
else
RESULTS+=("${YELLOW}WARN${RESET} $name ${DIM}(advisory only)${RESET}")
((WARN++))
fi
}

echo ""
printf "${BOLD}pre-push checks${RESET}\n"
echo "────────────────────────────────────────"

# 1. Security audits
run_check "security audit (backend)" node "$HOOKS_DIR/audit-grace.cjs" "$REPO_ROOT/backend"
run_check "security audit (frontend)" node "$HOOKS_DIR/audit-grace.cjs" "$REPO_ROOT/frontend"

# 2. Type checking
run_check "typecheck (backend)" npx --prefix "$REPO_ROOT/backend" tsc --noEmit --project "$REPO_ROOT/backend/tsconfig.json"
run_check "typecheck (frontend)" npx --prefix "$REPO_ROOT/frontend" tsc --noEmit --project "$REPO_ROOT/frontend/tsconfig.json"

# 3. Lint (advisory — does not block push until existing issues are resolved)
run_advisory "eslint (frontend)" npm run lint --prefix "$REPO_ROOT/frontend"

# Summary
echo ""
echo "────────────────────────────────────────"
for r in "${RESULTS[@]}"; do
printf " %b\n" "$r"
done
echo "────────────────────────────────────────"

if [[ $FAIL -gt 0 ]]; then
printf "\n ${RED}${BOLD}Push blocked${RESET}: %d check(s) failed.\n\n" "$FAIL"
exit 1
fi

if [[ $WARN -gt 0 ]]; then
printf "\n ${YELLOW}${BOLD}Warnings${RESET}: %d advisory(s) in grace period. Push allowed.\n\n" "$WARN"
fi

printf " ${GREEN}${BOLD}All checks passed.${RESET}\n\n"
exit 0
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ next-env.d.ts
.DS_Store
.vercel
coverage

# Git hook audit cache (generated by .githooks/audit-grace.cjs)
.githooks/.audit-cache
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ Open `http://localhost:3000`.

**DOC or DOCX conversion fails.** Install LibreOffice locally and restart the backend so document conversion commands are available on the process path.

## Git Hooks

This repo ships shared git hooks for pre-push quality checks. Enable them once:

```bash
git config core.hooksPath .githooks
```

What runs on push:
- **npm audit** — critical/high vulnerabilities are blocked, with a 24-hour grace period for newly published advisories
- **TypeScript type checking** — backend and frontend
- **ESLint** — frontend (advisory only; warns but does not block)

Known/accepted vulnerabilities are documented in `.githooks/audit-known.json` with expiration dates.

## Useful Checks

```bash
Expand Down
1 change: 1 addition & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ R2_BUCKET_NAME=mike
GEMINI_API_KEY=your-gemini-key
ANTHROPIC_API_KEY=your-anthropic-key
OPENAI_API_KEY=your-openai-key
CONCENTRATE_API_KEY=your-concentrate-key
RESEND_API_KEY=your-resend-key
USER_API_KEYS_ENCRYPTION_SECRET=your-long-random-secret
13 changes: 13 additions & 0 deletions backend/migrations/001_add_concentrate_provider.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Add 'concentrate' as a valid provider in user_api_keys.
--
-- PostgreSQL does not support adding a value to a CHECK constraint directly;
-- the constraint must be dropped and recreated. The table has no data
-- dependency on the old constraint shape — existing rows with provider in
-- ('claude', 'gemini', 'openai') continue to satisfy the new constraint.

ALTER TABLE public.user_api_keys
DROP CONSTRAINT IF EXISTS user_api_keys_provider_check;

ALTER TABLE public.user_api_keys
ADD CONSTRAINT user_api_keys_provider_check
CHECK (provider IN ('claude', 'gemini', 'openai', 'concentrate'));
Loading