Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/api.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 |
| Branch | master |
| Path or archive member | frontend/src/lib/api.ts |
| Original SHA-256 | 41d44c267c5edcaa7f672377498181daf534f19495acde9d33cd36ceb1879046 |
| Git blob SHA | f74b97b172e2e674c91180c2e3ebaaca42698990 |
| Reading-copy SHA-256 | 41d44c267c5edcaa7f672377498181daf534f19495acde9d33cd36ceb1879046 |
Every recorded occurrence
These 3 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / frontend/src/lib/api.ts | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / frontend/src/lib/api.ts | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / frontend/src/lib/api.ts | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
Recovered source transcript
Download complete source text ↓196 lines · 8.9 KB
import type {
AdverseImpactReport,
CurationProposalView,
CandidateListView,
ExternalCandidate,
FederalAward,
MissionCheckModeAResult,
MissionCheckModeBResult,
OpportunityFilters,
OpportunityFitView,
RankedCandidateView,
RequirementListView,
SamOpportunity,
SaveCandidateRequest,
ScreenView,
WageBaseline,
} from './types'
// opportunityQuery turns the filter object into a query string. Multi valued filters are repeated
// params (?naics=a&naics=b); the backend also accepts comma lists.
function opportunityQuery(f: OpportunityFilters): string {
const p = new URLSearchParams()
f.naics?.forEach((v) => p.append('naics', v))
f.set_aside?.forEach((v) => p.append('set_aside', v))
f.state?.forEach((v) => p.append('state', v))
f.department?.forEach((v) => p.append('department', v))
f.sub_agency?.forEach((v) => p.append('sub_agency', v))
if (f.q) p.set('q', f.q)
if (f.active_only === false) p.set('active_only', 'false')
if (f.limit) p.set('limit', String(f.limit))
const s = p.toString()
return s ? `?${s}` : ''
}
export class ApiError extends Error {
status: number
constructor(status: number, message: string) {
super(message)
this.status = status
}
}
// csrfToken is the signed double submit token issued by GET /auth/csrf. The companion __Host-mm_csrf
// cookie is set by that response; the __Host-mm_session cookie rides along via credentials include, so
// the validated session is the tenant source, not a client header.
let csrfToken: string | null = null
let csrfInflight: Promise<string> | null = null
async function ensureCsrf(): Promise<string> {
if (csrfToken) return csrfToken
// De-duplicate concurrent mints behind one shared fetch. Without this, two unsafe requests racing on a
// null token each hit GET /auth/csrf, the second response overwrites the __Host-mm_csrf cookie, and the
// first request's header no longer matches its cookie, so the double submit check 403s. One mint per
// token lifetime keeps the cookie and every header in agreement.
if (!csrfInflight) {
csrfInflight = fetch('/api/auth/csrf', { credentials: 'include' })
.then(async (res) => {
if (!res.ok) {
throw new Error(`csrf token request failed: ${res.status}`)
}
const body = (await res.json()) as { csrf_token?: string }
const token = body.csrf_token
if (!token) {
throw new Error('csrf token request returned an empty token')
}
csrfToken = token
return token
})
.finally(() => {
// Clear the in flight marker on success and failure so a failed mint is never cached and a
// later request can retry rather than sending an empty token into a guaranteed 403 loop.
csrfInflight = null
})
}
return csrfInflight
}
// normalizeHeaders flattens any HeadersInit form (a plain object, a Headers instance, or a tuple array)
// to a plain record so the spread below is correct for every caller, not just plain objects.
function normalizeHeaders(h: HeadersInit | undefined): Record<string, string> {
if (!h) return {}
if (h instanceof Headers) return Object.fromEntries(h.entries())
if (Array.isArray(h)) return Object.fromEntries(h)
return { ...h }
}
async function doFetch(path: string, init: RequestInit, method: string): Promise<Response> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...normalizeHeaders(init.headers),
}
if (method !== 'GET' && method !== 'HEAD') {
headers['X-CSRF-Token'] = await ensureCsrf()
}
// credentials include sends the __Host-mm_session cookie; the validated session is the tenant source.
return fetch(`/api${path}`, { ...init, credentials: 'include', headers })
}
async function rawRequest(path: string, init?: RequestInit): Promise<Response> {
const method = (init?.method ?? 'GET').toUpperCase()
let res = await doFetch(path, init ?? {}, method)
// A stale or rotated CSRF token returns 403; refresh once and retry the unsafe verb.
if (res.status === 403 && method !== 'GET' && method !== 'HEAD') {
csrfToken = null
res = await doFetch(path, init ?? {}, method)
}
if (!res.ok) {
let detail = res.statusText
try {
// Two error body shapes are in use: a plain string error, and the AI brain's structured envelope
// { error: { code, message } } (a 422 refusal, for example). Read the message from whichever is present
// so a refusal reason is shown verbatim rather than stringified to an object.
const body = (await res.clone().json()) as { error?: string | { message?: string; code?: string } }
if (typeof body.error === 'string') {
detail = body.error
} else if (body.error) {
detail = body.error.message || body.error.code || detail
}
} catch {
/* ignore non-JSON error bodies */
}
throw new ApiError(res.status, detail)
}
return res
}
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await rawRequest(path, init)
return (await res.json()) as T
}
async function requestText(path: string): Promise<string> {
const res = await rawRequest(path)
return res.text()
}
export const api = {
candidates: () => request<CandidateListView[]>('/candidates?limit=200'),
// Fetch one candidate or requirement by id so a deep linked evaluation works even when the row is
// not inside the first list page; the list scan would silently render a blank evaluation otherwise.
candidate: (id: string) =>
request<CandidateListView>(`/candidates/${encodeURIComponent(id)}`),
requirements: () => request<RequirementListView[]>('/requirements?limit=200'),
requirement: (id: string) =>
request<RequirementListView>(`/requirements/${encodeURIComponent(id)}`),
screen: (candidate_id: string, requirement_id: string) =>
request<ScreenView>('/screen', {
method: 'POST',
body: JSON.stringify({ candidate_id, requirement_id }),
}),
opportunities: (candidateId: string) =>
request<OpportunityFitView[]>(`/candidates/${candidateId}/opportunities?limit=10`),
rankCandidates: (requirementId: string) =>
request<RankedCandidateView[]>(`/requirements/${requirementId}/candidates`),
// Cached SAM.gov opportunities (the requirement feed). The frontend NEVER calls SAM live.
samOpportunities: (f: OpportunityFilters = {}) =>
request<SamOpportunity[]>(`/opportunities${opportunityQuery(f)}`),
samOpportunity: (noticeId: string) =>
request<SamOpportunity>(`/opportunities/${encodeURIComponent(noticeId)}`),
// USASpending awards (cache only): incumbents by NAICS and Galapagos own past performance.
incumbents: (naics: string) =>
request<FederalAward[]>(`/awards?naics=${encodeURIComponent(naics)}&limit=6`),
galapagosAwards: () => request<FederalAward[]>('/awards/galapagos?limit=8'),
// BLS OEWS regional wage baseline (cache only).
wages: () => request<WageBaseline[]>('/wages'),
// Externally sourced candidates (People Data Labs) plus the delete path.
externalCandidates: () => request<ExternalCandidate[]>('/sourcing/candidates'),
deleteExternalCandidate: (id: string) =>
request<{ status: string }>(`/sourcing/candidates/${encodeURIComponent(id)}`, { method: 'DELETE' }),
// Compliance: the adverse impact report (compliance role only) and its publication ready export.
adverseImpact: () => request<AdverseImpactReport>('/compliance/adverse-impact'),
adverseImpactExport: () => requestText('/compliance/adverse-impact/export'),
// WS8 curation: the pending teaching proposals and the two step human gate (compliance or operator role).
// Approve stages an inert ontology edge; Verify (a separate action) activates it for future scores.
curationProposals: () => request<CurationProposalView[]>('/compliance/curation/proposals'),
approveCurationProposal: (id: string) =>
request<CurationProposalView>(`/compliance/curation/proposals/${encodeURIComponent(id)}/approve`, { method: 'POST' }),
verifyCurationProposal: (id: string) =>
request<CurationProposalView>(`/compliance/curation/proposals/${encodeURIComponent(id)}/verify`, { method: 'POST' }),
// Mission Check (W1): the resume is parsed to text in the browser; only the text crosses the wire.
// Mode A scores it against every open opportunity, Mode B against one pasted job description.
missionCheckOpportunities: (resume_text: string, limit = 10) =>
request<MissionCheckModeAResult>('/mission-check/opportunities', {
method: 'POST',
body: JSON.stringify({ resume_text, limit }),
}),
missionCheckJD: (resume_text: string, job_description_text: string) =>
request<MissionCheckModeBResult>('/mission-check/screen', {
method: 'POST',
body: JSON.stringify({ resume_text, job_description_text }),
}),
// Save Candidate: persist a reviewed Mission Check result as an audited candidate (recruiter capability).
saveCandidate: (body: SaveCandidateRequest) =>
request<{ id: string }>('/candidates', { method: 'POST', body: JSON.stringify(body) }),
}