Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ Annual discount 17%. 30-day money-back. ONE pricing grid. No Federal tier until

| Integration | Status | Action Required |
|-------------|--------|-----------------|
| Supabase auth + DB | ✅ Wired | Migrations through 034 in repo. Applied to prod: 001–027, plus **028 (rate-limit buckets), 031 (auth lockouts) and 032 (auth audit trail) applied 2026-08-12** — shared rate limiting, lockout, CAPTCHA escalation and the auth audit trail are now live. **Unapplied: 029 + 030** (seed-anchor chain — separate subsystem), **033** (restrictive deny-all on the Better Auth tables) and **034** (marketing opt-out column — CAN-SPAM). `/api/health` now reports the rate-limit and lockout stores as degraded when a migration is missing, instead of reporting green. |
| Supabase auth + DB | ✅ Wired | Migrations through 036 are in the repo. Applied-to-production status must be verified in the release record. **Release prerequisites:** 028 (shared rate-limit buckets), 031 (auth lockouts), 032 (auth audit trail), 034 (marketing opt-out column before commercial outreach), **035 (hash-only, one-time password-reset codes before reset is enabled)**, and **036 (revoke public execution of privileged RPCs)**. `/api/health` reports missing control stores and reset-code configuration as degraded rather than green. |
| Stripe checkout | ✅ Wired | Add a **$499 one-time** report SKU (Stage 1 primary product) |
| Stripe webhook | ⚠️ Verify URL | Confirm `https://www.houndshield.com/api/stripe/webhook` |
| STRIPE_WEBHOOK_SECRET | ❌ Verify | Confirm set in Vercel dashboard |
Expand Down Expand Up @@ -334,7 +334,7 @@ compliance-firewall-agent/
lib/brain-ai/ — BM25 knowledge graph + query interface
lib/gateway/ — Core AI interception engine
lib/classifier/ — 53-pattern / 16-engine CUI/PII/IP/PHI detector
supabase/migrations/ — through 034 in repo (029, 030, 033, 034 not yet applied to prod)
supabase/migrations/ — through 036 in repo (verify production application before release; 035 enables code-only reset and 036 removes public privileged-RPC execution)

proxy/
server.ts — HTTPS proxy (the actual product)
Expand Down
8 changes: 8 additions & 0 deletions compliance-firewall-agent/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ OPENROUTER_API_KEY=REPLACE_WITH_YOUR_OPENROUTER_API_KEY
# ─── Email (Resend) ───────────────────────────────────────
RESEND_API_KEY=REPLACE_WITH_YOUR_RESEND_API_KEY

# ─── Authentication security (required in production) ──────
# 32+ random bytes, base64/hex encoded; used to HMAC password-reset codes.
AUTH_RESET_CODE_PEPPER=REPLACE_WITH_A_32_BYTE_RANDOM_SECRET
# Cloudflare Turnstile: escalation fails closed when this secret is missing.
TURNSTILE_SECRET_KEY=REPLACE_WITH_YOUR_TURNSTILE_SECRET
# Public site key used only by the browser widget when CAPTCHA is requested.
NEXT_PUBLIC_TURNSTILE_SITE_KEY=REPLACE_WITH_YOUR_TURNSTILE_SITE_KEY

# ─── App ───────────────────────────────────────────────────
NEXT_PUBLIC_APP_URL=https://houndshield.com
ENCRYPTION_KEY=REPLACE_WITH_64_CHAR_HEX_STRING
Expand Down
11 changes: 6 additions & 5 deletions compliance-firewall-agent/app/__tests__/direction-a-port.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import { join } from 'node:path'
/**
* Content contract for the HERMES Direction-A exact-match port.
* Reads page source directly so it stays fast and dependency-free, and locks in:
* 1. the demo's verbatim copy on each ported view, and
* 2. the legal/strategy guardrails that must NEVER regress to the demo's literal text.
* 1. the demo's visual-information architecture on each ported view, and
* 2. the legal/strategy guardrails that must NEVER regress to unsupported demo copy.
* See docs/DIRECTION-A-PORT.md.
*/
const root = process.cwd()
const read = (p: string) => readFileSync(join(root, p), 'utf8')

describe('Direction-A port — demo copy present', () => {
it('home: comparison, features and CTA use the demo headlines', () => {
describe('Direction-A port — information architecture present', () => {
it('home: comparison, features and CTA retain the ported hierarchy without unsupported claims', () => {
const home = read('app/page.tsx')
expect(home).toContain("Cloud DLP scans your CUI in their cloud")
expect(home).toContain('Start with the boundary your assessor will ask about')
expect(home).not.toContain("Cloud DLP scans your CUI in their cloud")
expect(home).toContain('Everything you need for CMMC Level 2')
expect(home).toContain('Ready to protect your CUI?')
// demo comparison cards
Expand Down
36 changes: 21 additions & 15 deletions compliance-firewall-agent/app/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,13 @@ describe('HomePage — HERMES demo parity', () => {
})

// ── Hero ─────────────────────────────────────────────────────────
// Contract changed 2026-07-28: DoD suspended CMMC Phase 2 on 2026-07-13,
// removing the Nov 10 deadline the old CUI-first copy leaned on. The hero
// now leads with evidence ("prove what was pasted"), which sells to both a
// healthcare privacy officer and a contractor facing FCA/SPRS exposure.
it('H1 leads with the evidence promise, not the suspended CMMC deadline', () => {
// The hero leads with the control boundary rather than a claimed regulatory
// outcome or an unsupported assertion about every external AI provider.
it('H1 leads with the buyer-controlled boundary, not a compliance promise', () => {
render(<HomePage />)
const h1 = screen.getByRole('heading', { level: 1 })
expect(h1.textContent).toMatch(/Prove what your team pasted into/i)
expect(h1.textContent).toContain('ChatGPT')
expect(h1.textContent).toMatch(/Keep regulated data inside your control boundary/i)
expect(h1.textContent).not.toContain('ChatGPT')
})

it('hero pill leads with HIPAA and NIST, not a CMMC certification date', () => {
Expand Down Expand Up @@ -84,9 +82,9 @@ describe('HomePage — HERMES demo parity', () => {
expect(screen.getByText('Live prompt scans')).toBeTruthy()
})

it('hero trust row makes no free-tier promise (single $499 offer)', () => {
it('hero trust row distinguishes hosted evaluation from the self-hosted path', () => {
const { container } = render(<HomePage />)
for (const t of ['One URL change', 'Runs on your hardware', 'Nothing transmitted', 'Audit-ready PDF']) {
for (const t of ['Hosted evaluation clearly labelled', 'Self-hosted path for sensitive workloads', 'Your deployment, your boundary', 'Evidence-oriented PDF']) {
expect(container.textContent).toContain(t)
}
// The free tier was removed from /pricing; the hero must not re-promise it.
Expand All @@ -102,7 +100,15 @@ describe('HomePage — HERMES demo parity', () => {
expect(screen.getByText('NIST 800-171 controls')).toBeTruthy()
})

it('replaces the unverifiable "~80,000 contractors" stat with a sourced figure', () => {
/**
* Merge resolution (#302 <- main): this branch asserted a "2 / deployment
* paths" tile and the ABSENCE of 89%; main asserts the Netskope figure. The
* stat grid is a hard `repeat(4, 1fr)`, so only one tile fits and the source
* was resolved to main's. The deployment distinction is still asserted — by
* the Mode-B notice test below, which covers it more strictly than a stat
* tile ever did.
*/
it('replaces unverifiable market statistics with a sourced figure', () => {
const { container } = render(<HomePage />)
expect(container.textContent).not.toContain('~80,000')
expect(screen.getByText(REGULATED_SHARE_GENAI.value)).toBeTruthy()
Expand Down Expand Up @@ -132,9 +138,9 @@ describe('HomePage — HERMES demo parity', () => {
})

// ── Asymmetric advantage ─────────────────────────────────────────
it('renders the asymmetric-advantage headline (demo copy)', () => {
it('renders the evidence-first deployment-boundary headline', () => {
render(<HomePage />)
expect(screen.getByText(/Cloud DLP scans your CUI in their cloud/i)).toBeTruthy()
expect(screen.getByText(/Start with the boundary your assessor will ask about/i)).toBeTruthy()
})

it('renders the demo 3-card comparison (Nightfall & Strac / Purview / HoundShield)', () => {
Expand Down Expand Up @@ -200,13 +206,13 @@ describe('HomePage — HERMES demo parity', () => {
expect(container.textContent).not.toMatch(/500\+\s*teams|2M\+\s*scans/i)
})

it('matches the demo section order: hero → statsasymmetric → platform → CTA', () => {
it('keeps the conversion order: boundary hero → proofcomparison → platform → CTA', () => {
const { container } = render(<HomePage />)
const text = container.textContent ?? ''
const order = [
'Prove what your team pasted into',
'Keep regulated data inside',
'Detection engines',
'Cloud DLP scans your CUI in their cloud',
'Start with the boundary your assessor will ask about',
'Everything you need for CMMC Level 2',
'Ready to protect your CUI?',
].map((s) => text.indexOf(s))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,13 @@ describe('password reset does not distinguish a known address from an unknown on
expect(src).toMatch(/const\s+ok\s*=\s*\(\)\s*=>\s*NextResponse\.json\(\{\s*ok:\s*true/);
});

it('keeps the email send off the response path', () => {
it('keeps code email delivery off the response path', () => {
// after() bounds the slow half; the timing floor bounds the fast half.
expect(src).toMatch(/after\(\(\)\s*=>\s*sendPasswordResetEmail/);
expect(src).toMatch(/after\(\(\)\s*=>\s*sendPasswordResetCodeEmail/);
});

it('does not construct or dispatch a URL-borne recovery token', () => {
expect(src).not.toMatch(/generateLink|buildRecoveryConfirmUrl|token_hash|auth\/confirm/);
});

it('never returns a 404 for an unknown account', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,11 @@ describe('lockout', () => {
});

describe('rollback and configuration', () => {
it('answers 501 when AUTH_SERVER_ROUTES=off, without touching the provider', async () => {
it('answers a generic 503 when local server auth is disabled, without touching the provider', async () => {
process.env.AUTH_SERVER_ROUTES = 'off';
const res = await POST(req(creds));
expect(res.status).toBe(501);
expect(res.status).toBe(503);
expect((await res.json()).error).toBe('Authentication is unavailable in this development environment.');
expect(mockSignIn).not.toHaveBeenCalled();
expect(mockGuard).not.toHaveBeenCalled();
});
Expand Down
Loading
Loading