This document outlines security vulnerabilities and issues found in the Bounteer codebase that need to be addressed before deploying to production.
Severity: CRITICAL
Location: src/constant.ts:4
directus_key: "9Qfz6A4s0RjzSPLLIR0yT7NTptiPfTGQ", // guest accountIssue: The Directus API key is hardcoded directly in the source code and committed to the public GitHub repository. This key is exposed to anyone who can view the repository.
Impact:
- Attackers can use this key to make unauthorized API requests to your Directus instance
- Even though labeled as "guest account", this provides access to your backend
- Key is used throughout the codebase for Bearer token authentication
- Found in 10+ locations across the codebase
Files using this key:
src/lib/utils.ts:321src/components/interactive/JobDescriptionCard.tsx:74src/components/interactive/OrbitCallDashboard.tsx:305,752src/components/interactive/ReportCard.tsx:177,188,255src/scripts/cvUploader.js:47,59,101
Solution:
- Move API key to environment variables
- Create
.envfile (add to.gitignore) - Use
import.meta.env.PUBLIC_DIRECTUS_KEYor similar for client-side - Use
import.meta.env.DIRECTUS_KEYfor server-side only - Rotate the current API key immediately (it's already compromised)
- Set up proper Directus permissions to limit guest account access
- Consider using Directus public roles instead of a guest token
Severity: HIGH
Location: src/constant.ts:5
guest_user_id: "f25f8ce7-e4c9-40b8-ab65-40cde3409f27", // guest user idIssue: The guest user UUID is hardcoded in source code.
Impact:
- Attackers know which user ID represents guest users
- Could be used to bypass authentication checks or forge guest requests
- May enable privilege escalation if not properly secured in Directus
Solution:
- Move to environment variable
- Review Directus permissions for the guest user
- Ensure guest user has minimal read-only permissions
- Consider if guest user is even necessary
Severity: HIGH
Location: src/components/interactive/CoverLetterCard.tsx:58-62
tempDiv.innerHTML = `
<div style="text-align: center; margin-bottom: 40px; border-bottom: 2px solid #000; padding-bottom: 20px;">
<h1 style="margin: 0; font-size: 24px; font-weight: bold;">Cover Letter</h1>
</div>
<div style="white-space: pre-wrap; text-align: justify;">${editableCoverLetter}</div>
`;Issue: User-provided content (editableCoverLetter) is directly interpolated into innerHTML without sanitization.
Impact:
- Cross-Site Scripting (XSS) attack vector
- Attackers can inject malicious JavaScript
- Could steal user data, cookies, or session tokens
- Could redirect users to phishing sites
Solution:
- Sanitize HTML content using a library like DOMPurify
- Use
textContentinstead ofinnerHTMLif HTML formatting isn't needed - Implement Content Security Policy (CSP) headers
- Review other uses of
innerHTMLin codebase:src/components/interactive/MagicGlowEffect.tsxsrc/pages/unsubscribe.astro
Severity: MEDIUM
Location: src/components/interactive/ContactForm.tsx:41-45
const res = await fetch(`${EXTERNAL.directus_url}/items/message`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});Issue: Contact form submissions don't include authentication headers, relying on Directus being openly accessible for POST requests to /items/message.
Impact:
- If Directus permissions aren't configured correctly, anyone can spam the message collection
- No rate limiting on form submissions
- Could be used for DoS attacks
- Spam/bot submissions aren't prevented
Solution:
- Review Directus permissions for
messagecollection - Implement rate limiting (client-side and server-side)
- Add CAPTCHA or similar bot protection
- Consider using the guest token for this endpoint
- Add input validation on server-side (Directus)
- Set up webhook notifications for new messages
Severity: MEDIUM Location: Multiple files (see list below)
Issue: Authorization headers with Bearer tokens are used extensively in client-side code.
Affected files:
src/components/interactive/JobDescriptionCard.tsx:74src/components/interactive/OrbitCallDashboard.tsx:305,752src/components/interactive/ReportCard.tsx:177,188src/scripts/cvUploader.js:47,59,101
Impact:
- API keys visible in browser DevTools Network tab
- Can be extracted and reused by attackers
- Guest token has whatever permissions assigned to it
Solution:
- Minimize use of guest token in client-side code
- Rely on user session cookies for authenticated requests where possible
- For public data, use Directus public role instead of guest token
- Implement server-side proxy for sensitive operations
- Review and minimize permissions on guest account
- IMMEDIATELY - Rotate the exposed Directus API key (
9Qfz6A4s0RjzSPLLIR0yT7NTptiPfTGQ) - IMMEDIATELY - Remove the hardcoded key from
src/constant.ts - IMMEDIATELY - Set up environment variables for all secrets
- IMMEDIATELY - Update
.gitignoreto include.env* - IMMEDIATELY - Fix XSS vulnerability in CoverLetterCard.tsx
- Review Directus permissions and minimize guest account access
- Implement HTML sanitization across the codebase
- Set up proper webhook endpoint and authentication
- Add rate limiting to public forms
- Implement Content Security Policy headers
- Set up security scanning in CI/CD pipeline
- Implement API request monitoring and alerting
- Add security headers (CSP, X-Frame-Options, etc.)
- Regular security audits of dependencies
- Implement proper logging and audit trails
- Consider Web Application Firewall (WAF)
- Set up vulnerability disclosure policy
- Review all Directus role permissions
- Audit Directus access logs for suspicious activity
- Check if the exposed API key has been used by unauthorized parties
- Review CORS configuration on Directus
- Ensure HTTPS is enforced everywhere
- Check for SQL injection vulnerabilities in custom Directus flows/hooks
- Review file upload security (size limits, file type validation)
- Ensure proper session management and timeout settings
- Check for CSRF protection on state-changing operations
Last Updated: 2025-11-24 Audited By: Claude Code Security Audit
Add candidate-focused mode to Orbit Call, enabling candidates to upload their profile and search for matching jobs. This complements the existing company mode (job description → candidate search) with the reverse flow (candidate profile → job search).
Implementation Approach: Single page with mode toggle (not separate pages)
- Page:
src/pages/orbit.astro(existing, to be enhanced) - Component:
OrbitCallDashboard.tsx(add mode toggle) - Toggle Location: Top of orange gradient card (not_linked stage)
type OrbitMode = "company" | "candidate";┌─────────────────────────────────────────────────────────┐
│ 🔄 [Company Search] [Candidate Search] ← Mode Toggle │
│ │
│ Set Up New Orbit Call │
│ [Meeting] [Testing] │
│ [URL Input ________________] [Deploy] │
└─────────────────────────────────────────────────────────┘
| Aspect | Company Mode (Current) | Candidate Mode (New) |
|---|---|---|
| Title | "Job Description Enrichment" | "Candidate Profile Enrichment" |
| Component | JobDescriptionEnrichment.tsx |
CandidateProfileEnrichment.tsx (NEW) |
| Action Button | "Search People" | "Search Jobs" |
| Results Section | "Potential Candidates" | "Matching Jobs" |
| Gradient Color | Orange (#ff6b35) | Green/Teal (#10b981) |
| Data Model | JobDescriptionFormData |
CandidateProfileFormData (NEW) |
- Get
candidate_profileschema from Directus database - Create
src/schemas/directus.tsfor centralized schema definitions - Define
CandidateProfileSchemainterface - Define
CandidateProfileFormDatatype - Define
JobSearchResultinterface - Migrate existing types from
src/types/models.tsto use schemas
- Add
orbitModestate toOrbitCallDashboard.tsx - Add mode toggle buttons in
renderNotLinkedStage()(line ~618) - Add candidate-specific state variables:
-
candidateDatastate -
candidateProfileIdstate -
jobSearchRequestIdstate -
jobsstate -
jobSearchWsRefWebSocket reference
-
- Implement mode-aware gradient colors
- Add conditional rendering based on
orbitMode
- Create
CandidateProfileEnrichment.tsx- Mirror structure of
JobDescriptionEnrichment.tsx - 3-stage flow: not_linked → ai_enrichment → manual_enrichment
- AI/Manual toggle switch
- Form fields for candidate information
- WebSocket/polling for real-time updates
- Save functionality
- Green/Teal gradient theme
- Mirror structure of
- Create
JobSearchResults.tsx- Card-based layout for matched jobs
- Job fit percentage visualization
- Job details display
- Horizontal scrollable layout
- Add candidate API functions to
src/lib/utils.ts:-
createOrbitCandidateCallRequest() -
createCandidateProfile() -
updateCandidateProfile() -
createJobSearchRequest() -
fetchJobSearchResults()
-
- Implement WebSocket subscription for job search status
- Handle job search request lifecycle
- Test company mode (ensure no regressions)
- Test candidate mode end-to-end
- Test mode switching behavior
- Test with both meeting and testing input modes
- Verify WebSocket connections
- Test form validation
- Test save functionality
// Pending: Get actual schema from Directus database
export interface CandidateProfileSchema {
id?: string;
first_name?: string;
last_name?: string;
email?: string;
phone?: string;
location?: string;
year_of_experience?: number;
current_title?: string;
company_name?: string;
education?: string;
summary?: string;
skills?: string[]; // JSON array
work_history?: string;
achievements?: string;
// ... other fields from database
date_created?: string;
date_updated?: string;
}
export type CandidateProfileFormData = Omit<
CandidateProfileSchema,
'id' | 'date_created' | 'date_updated'
>;interface Job {
id: string;
title: string;
company: string;
location: string;
experience: string;
jobFitPercentage: number;
skills: string[];
salary?: string;
}Based on existing company flow pattern:
orbit_candidate_call_request(similar toorbit_call_request)orbit_candidate_call_session(similar toorbit_call_session)candidate_profile(EXISTING - need schema)job_search_request(similar toorbit_candidate_search_request)job_search_result(similar toorbit_candidate_search_result)
- Company Mode: rgb(255, 154, 0) → rgb(255, 87, 34) [Orange]
- Candidate Mode: rgb(16, 185, 129) → rgb(5, 150, 105) [Green/Teal]
// Active state
className="bg-white text-black hover:bg-gray-200"
// Inactive state
className="bg-white/20 border-white/40 text-white hover:bg-white/30 backdrop-blur-sm"✅ Unified interface (single page for both flows) ✅ Consistent UX (same patterns and interactions) ✅ Easy mode switching without navigation ✅ Code reuse (shared URL validation, WebSocket logic) ✅ Maintainability (parallel structures)
- What are the exact fields in the Directus
candidate_profilecollection? - Are job postings stored in Directus or external API?
- Do candidates and companies use different authentication/roles?
- Should the page default to company or candidate mode?
src/schemas/directus.ts(centralized schema definitions)src/components/interactive/CandidateProfileEnrichment.tsxsrc/components/interactive/JobSearchResults.tsx
src/components/interactive/OrbitCallDashboard.tsx(add mode toggle)src/types/models.ts(migrate to use schemas)src/lib/utils.ts(add candidate API functions)
- Current implementation:
src/components/interactive/OrbitCallDashboard.tsx:31-844 - Enrichment pattern:
src/components/interactive/JobDescriptionEnrichment.tsx:1-773 - Form data types:
src/types/models.ts:65-118
Feature Status: Planning / Documentation Phase Last Updated: 2025-11-27 Priority: Medium Blocked By: Need candidate_profile schema from database
Status: Approved - 2025-11-29
See detailed analysis in: comment.md
Decision: Deprecate orbit_call_session in favor of two specific enrichment session collections.
Rationale:
- ✅ Better separation of concerns (job enrichment vs candidate enrichment)
- ✅ Clearer purpose and intent
- ✅ Aligns with bidirectional architecture
- ✅ Eliminates ambiguity about session type
IMPORTANT: Rename generic session field to specific enrichment session type
-
Frontend code updated to use
job_enrichment_session(2025-11-29)- Updated
OrbitCandidateSearchRequesttype insrc/lib/utils.ts - Updated
createOrbitCandidateSearchRequestto usejob_enrichment_sessionfield - Updated all components to use
orbit_job_description_enrichment_session
- Updated
-
⚠️ DATABASE MIGRATION REQUIRED: Rename field inorbit_candidate_search_request- Current:
session(FK → orbit_call_session) ❌ Generic, ambiguous - New:
job_enrichment_session(FK → orbit_job_description_enrichment_session) ✅ - Reason: Recruiters enrich a job, then search for candidates
- SQL:
-- Step 1: Add new column ALTER TABLE orbit_candidate_search_request ADD COLUMN job_enrichment_session INTEGER REFERENCES orbit_job_description_enrichment_session(id); -- Step 2: Migrate data from old session field UPDATE orbit_candidate_search_request SET job_enrichment_session = session; -- Step 3: Drop old column (after verifying migration) ALTER TABLE orbit_candidate_search_request DROP COLUMN session;
- Current:
-
Create
orbit_job_search_requestwith correct field name- Field:
candidate_enrichment_session(FK → orbit_candidate_profile_enrichment_session) - NOT:
session(too generic) - Reason: Candidates enrich their profile, then search for jobs
- Pattern: Mirror
orbit_candidate_search_requeststructure - Reference: comment.md section 4
- Field:
-
Create
orbit_job_search_resultcollection- Fields:
id,request(FK),jfi_score(integer 0-100),job_description(FK),rag_score,pros(json),cons(json), timestamps - Purpose: Store job matches with fit scores
- Pattern: Mirror
orbit_candidate_search_resultstructure - Reference: comment.md section 5
- Fields:
Current Issue: orbit_search_request references deprecated orbit_call_session
-
Update
orbit_search_requestschema- Current: Has
orbit_call_sessionFK ❌ - New: Replace with
job_enrichment_sessionFK ✅ - SQL:
-- Add new column ALTER TABLE orbit_search_request ADD COLUMN job_enrichment_session INTEGER REFERENCES orbit_job_description_enrichment_session(id); -- Migrate data: find matching enrichment session for each call session UPDATE orbit_search_request osr SET job_enrichment_session = ( SELECT id FROM orbit_job_description_enrichment_session ojdes WHERE ojdes.request = ( SELECT request FROM orbit_call_session ocs WHERE ocs.id = osr.orbit_call_session ) AND ojdes.job_description = osr.job_description LIMIT 1 ); -- Verify migration SELECT COUNT(*) FROM orbit_search_request WHERE job_enrichment_session IS NULL; -- Drop old column ALTER TABLE orbit_search_request DROP COLUMN orbit_call_session;
- Current: Has
-
Clarify
orbit_search_requestpurpose- Is this still used? Or is it replaced by
orbit_candidate_search_request? - If deprecated: Add deprecation notice, schedule deletion
- If active: Document how it differs from
orbit_candidate_search_request
- Is this still used? Or is it replaced by
-
Audit codebase for
orbit_call_sessionusage- Search frontend code (TypeScript/TSX files)
- Search API calls to
/items/orbit_call_session - Check Directus flows/hooks/webhooks
- Document all usage locations
-
Replace
orbit_call_sessionwith enrichment sessions- Move
host_userfield toorbit_call_request(parent level) - OR add
host_userto both enrichment sessions - Update all code references to use specific enrichment sessions
- Move
-
Add deprecation notice in Directus
- Mark collection as deprecated
- Add note: "Use orbit_job_description_enrichment_session or orbit_candidate_profile_enrichment_session"
- Set deprecation date: 2025-12-01
-
Remove
orbit_call_sessioncollection- Wait 30-90 days after deprecation notice
- Verify no active usage
- Backup existing data
- Drop collection
-
Add
modefield toorbit_call_request(NOT orbit_call_session - that's deprecated)- Type:
enum("recruiter", "candidate") - Required: YES (after backfill)
- Purpose: Identify which mode the call request is using
- Migration: Set existing records to "recruiter"
- Location: Parent level (orbit_call_request) since it applies to the entire call
- Type:
-
Add
host_usertoorbit_call_request(move from deprecated orbit_call_session)- Type:
uuid(FK → users) - Nullable: NO
- Purpose: Track who initiated the call
- Migration: Copy from existing orbit_call_session records before deletion
- Type:
- Fix
candidate_profile.year_of_experience- Current:
string❌ - Should be:
integerorfloat - Impact: Enables numeric comparisons for job matching
- Migration: Parse existing string values to integers
- Current:
Missing fields needed for job search:
- Add
first_name: string - Add
last_name: string - Add
email: string - Add
phone: string - Add
linkedin_url: string - Add
education: text - Add
preferred_locations: json(array of strings) - Add
remote_preference: enum("remote", "hybrid", "onsite", "flexible") - Add
preferred_employment_types: json(array: ["full-time", "contract"]) - Add
availability: string("immediately", "2 weeks", etc.) - Add
work_authorization: string("US Citizen", "H1B", etc.) - Add
resume_file: uuid(FK → directus_files) - Add
career_goals: text - Add
salary_expectation_min: integer - Add
salary_expectation_max: integer
Missing fields for better matching:
- Add
remote_policy: enum("remote", "hybrid", "onsite") - Add
employment_type: enum("full-time", "part-time", "contract") - Add
seniority_level: enum("entry", "junior", "mid", "senior", "lead") - Add
visa_sponsorship: boolean - Add
is_active: boolean - Verify
skill: jsonfield exists (mentioned in code but not seen in schema)
-
Create enum type for
search_request_status- Values: "pending", "processing", "listed", "failed"
- Apply to:
orbit_candidate_search_request,orbit_job_search_request
-
Document valid values for existing string fields:
orbit_call_session.modecandidate_profile.employment_typecandidate_profile.remote_preferencejob_description.backfill_status
-
Add NOT NULL constraints to critical FKs:
orbit_candidate_search_request.sessionorbit_candidate_search_result.requestorbit_job_search_request.sessionorbit_job_search_result.request
-
Add CHECK constraints for score ranges:
orbit_candidate_search_result.rfi_score(0-100)orbit_job_search_result.jfi_score(0-100)
-
Add enum constraints where applicable (see P1 item 6)
- Create index:
idx_orbit_call_session_modeonorbit_call_session(mode) - Create index:
idx_orbit_call_session_host_useronorbit_call_session(host_user) - Create index:
idx_candidate_search_request_statusonorbit_candidate_search_request(status) - Create index:
idx_candidate_search_result_rfi_scoreonorbit_candidate_search_result(rfi_score DESC) - Create index:
idx_job_search_request_statusonorbit_job_search_request(status) - Create index:
idx_job_search_result_jfi_scoreonorbit_job_search_result(jfi_score DESC) - Create composite index:
idx_candidate_profile_location_experience - Create composite index:
idx_job_description_location_active
-
Add to
candidate_profile:consent_given: booleanconsent_date: timestampdata_retention_until: dateis_public: boolean
-
Review and configure Directus permissions:
- Candidate role: Can manage own profile, view matched jobs only
- Recruiter role: Can manage job descriptions, view matched candidates only
- Admin role: Full access
-
Implement row-level security for sensitive data
-
Investigate
orbit_search_requestcollection- Document: What is this used for?
- Question: Is it deprecated?
- Action: Rename, consolidate, or remove
-
Investigate
orbit_search_resultcollection- Document: How does it differ from
orbit_candidate_search_result? - Question: Is it for job search or deprecated?
- Action: Clarify purpose or deprecate
- Document: How does it differ from
-
Document
role_fit_index_submission- What is this array?
- How is it used in scoring?
-
Create
reference/schema-docs.mdwith:- Field descriptions for all collections
- Valid enum values
- Relationship cardinality
- Required vs optional fields
- JSON field structures
- Score calculation methods
-
Add inline comments to schema fields in Directus
-
Document status lifecycles (pending → processing → listed → failed)
-
Document migration steps for:
- Adding
orbit_call_session.modefield - Backfilling mode = "recruiter" for existing sessions
- Converting
year_of_experiencefrom string to integer - Adding new nullable fields
- Adding
-
Create rollback plan for schema changes
- Job Source: Are jobs stored in Directus
job_descriptionor fetched from external API? - Authentication: Do candidates and recruiters have different Directus roles/permissions?
- AI Scoring: Where are RFI/RAG scores computed? (Backend service? Directus Flow?)
- WebSocket: Is status update WebSocket Directus built-in or custom implementation?
- Deprecated Collections: Can we safely remove
orbit_search_request/result? - Data Retention: What's the policy for cleaning up old search requests and snapshots?
Blocked by Backend/DB Team:
- All P0 schema changes (critical path)
- Understanding of deprecated collections
- Confirmation of job source (Directus vs external)
Blocked by Product:
- Default mode decision (recruiter vs candidate)
- Privacy/consent requirements
- Data retention policies
Can proceed in parallel:
- Frontend UI development (with mock data)
- Component creation (CandidateProfileEnrichment, JobSearchResults)
- API utility functions (with TypeScript interfaces)
Schema Review Status: Complete - See comment.md Last Updated: 2025-11-27 Reviewer: Claude Code Priority: P0 items are critical for bidirectional functionality
Add action tracking to Orbit Signal dashboard, allowing users to mark signals as "move to actions" or "ignore", with automatic filtering and organization.
- ✅ Action tracking using
hiring_intent_actiontable - ✅ "Move to Actions" and "Skip" buttons on each signal card
- ✅ Dedicated "Actions" section displaying signals with completed status
- ✅ Automatic removal of skipped signals from UI
- ✅ Real-time state updates when actions are created
- ✅ Count badges for both Actions and Orbit Signal sections
- ✅ Visual distinction between pending and actioned signals
- ✅ Proper relationship loading (hiring_intent with actions)
-
Type Updates:
src/lib/utils.ts:1472-1497- Added
HiringIntentActiontype for action records - Updated
HiringIntenttype to includeactions?: HiringIntentAction[]array
- Added
-
API Functions:
src/lib/utils.ts:1549-1603- Added
createHiringIntentAction()to create action records in hiring_intent_action table - Updated
getHiringIntentsBySpace()to fetch related actions using Directus field expansion
- Added
-
Component Updates:
src/components/interactive/HiringIntentDashboard.tsx- Added action buttons (Move to Actions, Skip) with icons
- Implemented
hasActionStatus()helper to check action array - Filtering logic checks actions array for completed/skipped status
- Created separate sections: "Actions" and "Orbit Signal"
- Real-time state management adds new action records to intents
- Conditional rendering based on action existence
Using Existing hiring_intent_action Table
The implementation uses the existing hiring_intent_action collection to track user actions on signals.
Schema Structure:
hiring_intent_action:
- id: integer (PK)
- intent: integer (FK → hiring_intent)
- category: string (e.g., 'user_action')
- status: enum ('pending', 'completed', 'skipped')
- user_created: uuid (FK → users)
- date_created: timestamp
- user_updated: uuid (FK → users)
- date_updated: timestamp
Status Mapping:
completed= Signal marked as "Move to Actions"skipped= Signal marked as "Skip"- No action = Pending signal (no action record)
- Appears at the top when signals are added to actions
- Green badge with count
- Cards without action buttons (already actioned)
- Visually separated from pending signals
- Shows pending/new signals only
- Each card has "Move to Actions" (green) and "Skip" (red) buttons
- Badges show count of pending signals
- Skipped signals are completely removed from view
- Verify
hiring_intent_actiontable exists in Directus - Test "Move to Actions" button creates action with status='completed'
- Test "Skip" button creates action with status='skipped'
- Verify skipped signals disappear from UI
- Verify actioned signals appear in Actions section
- Test with multiple spaces
- Test with space filter (All vs specific space)
- Verify count badges update correctly
- Test on mobile responsive layout
- Verify actions array is properly fetched with hiring_intents
- Add "Undo" functionality to remove action records
- Add bulk actions (select multiple signals)
- Add export functionality for actioned signals
- Add filtering/tabs by action status (All, Pending, Actions, Skipped)
- Add notes field to
hiring_intent_actionfor user comments - Add notification when new signals arrive
- Add CRM integration for actioned signals
- Show action history (who acted when) with user_created info
- Add action analytics dashboard
src/lib/utils.ts- Type updates, addedcreateHiringIntentAction(), updated fetch querysrc/components/interactive/HiringIntentDashboard.tsx- UI and logic implementation with action array handlingTODO.md- Updated documentation to reflecthiring_intent_actiontable usage
- Lucide React icons:
CheckCircle2,XCircle(already imported) - Button component from ShadCN (already available)
- Badge component from ShadCN (already available)
- Directus
hiring_intent_actioncollection (already exists)
Feature Status: ✅ Completed - Using Existing Database Schema Implemented: 2025-12-22 Developer: Claude Code Priority: P1 - Production Ready