Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/federalsignal/fetch.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/federalsignal/fetch.go |
| Original SHA-256 | 3a9094238f9f82d31c92aae142d0cfd6bffa42bcf3978a1ab9d7c7ed7543f4f8 |
| Git blob SHA | 04852bc2bf22c731afb6b6d5d7cb729952caf09a |
| Reading-copy SHA-256 | 37108a3fb8f8f40e0d31906ab401f6a5c692033e6c63a32e66240dfa0994648f |
Every recorded occurrence
These 15 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 / internal/modules/federalsignal/fetch.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / internal/modules/federalsignal/fetch.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓283 lines · 9.6 KB
Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.
// Package federalsignal mines public federal solicitation attachments for the deterministic mission and
// labor classifier. This file is the server side SAM attachment fetch client: it downloads a notice
// description and its resource links, runs each body through the pure Go docextract extractor, and
// returns the concatenated plain text. The SAM api_key is held here and only ever appended as a query
// parameter to a SAM host. It is NEVER logged, NEVER placed in an error string, and NEVER returned in
// the extracted text. The client is best effort: one bad attachment is skipped, never fatal.
package federalsignal
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/missionworkforce/missionmatch/internal/platform/docextract"
"github.com/missionworkforce/missionmatch/internal/platform/storage"
)
const (
defaultTimeout = 45 * time.Second
defaultMaxRetries = 2
maxBodyBytes = 8 << 20 // cap one download so a huge attachment cannot exhaust memory
maxTotalBytes = 4 << 20 // cap the concatenated text across all attachments for one record
separator = "\n\n----\n\n"
backoffStep = 200 * time.Millisecond
)
// Fetcher downloads SAM solicitation attachments and extracts their text. Build one with New. It is safe
// for concurrent use, the http.Client and the option fields are read only after construction.
type Fetcher struct {
key string // SAM api_key, server side only, never logged or returned
http *http.Client // bounded by Timeout
maxRetry int // additional attempts on a transient failure
samHosts map[string]struct{} // hosts the key may be appended to, lowercased
store storage.Storer // optional, nil skips retention
storePref string // key prefix for retained raw bytes
}
// Option configures a Fetcher.
type Option func(*Fetcher)
// WithTimeout sets the per download timeout.
func WithTimeout(d time.Duration) Option {
return func(f *Fetcher) {
if d > 0 {
f.http.Timeout = d
}
}
}
// WithMaxRetries sets the number of additional attempts on a transient download failure.
func WithMaxRetries(n int) Option {
return func(f *Fetcher) {
if n >= 0 {
f.maxRetry = n
}
}
}
// WithSAMHosts registers the hosts the api_key may be appended to. The key is sent ONLY to these hosts,
// so a foreign resource link in a notice can never receive the secret. Hosts are matched case
// insensitively on the URL host (with any port). Production registers api.sam.gov and sam.gov; the test
// registers the httptest host.
func WithSAMHosts(hosts ...string) Option {
return func(f *Fetcher) {
for _, h := range hosts {
h = strings.ToLower(strings.TrimSpace(h))
if h != "" {
f.samHosts[h] = struct{}{}
}
}
}
}
// WithStore wires an optional object store for raw attachment retention under the given key prefix. A
// nil store (or not calling this option) skips retention entirely.
func WithStore(s storage.Storer, prefix string) Option {
return func(f *Fetcher) {
f.store = s
f.storePref = strings.TrimSpace(prefix)
}
}
// New builds a Fetcher holding the SAM api_key. Defaults: a 45 second timeout, two retries, the two
// production SAM hosts, and no retention. Options override these.
func New(samKey string, opts ...Option) *Fetcher {
f := &Fetcher{
key: samKey,
http: &http.Client{Timeout: defaultTimeout},
maxRetry: defaultMaxRetries,
samHosts: map[string]struct{}{
"api.sam.gov": {},
"sam.gov": {},
},
}
for _, o := range opts {
o(f)
}
return f
}
// FetchSolicitationText downloads the notice description URL and each resource link, extracts the plain
// text of each, and returns the concatenated text capped at maxTotalBytes. It is best effort: a per
// attachment failure is counted and logged (never with the URL or the key) and skipped, it never fails
// the whole record. The api_key is [credential removed] only to a registered SAM host and never appears in the
// returned string.
func (f *Fetcher) FetchSolicitationText(ctx context.Context, descURL string, resourceLinks []string) (string, error) {
urls := make([]string, 0, 1+len(resourceLinks))
if strings.TrimSpace(descURL) != "" {
urls = append(urls, descURL)
}
for _, l := range resourceLinks {
if strings.TrimSpace(l) != "" {
urls = append(urls, l)
}
}
if len(urls) == 0 {
return "", nil
}
var sb strings.Builder
failures := 0
for _, raw := range urls {
if err := ctx.Err(); err != nil {
return sb.String(), err // a cancelled context is the caller's signal, not a per attachment skip
}
text, err := f.fetchOne(ctx, raw)
if err != nil {
// Count the failure without echoing the URL (it carries the api_key once appended) or the cause
// detail that could include it. Best effort: continue to the next attachment.
failures++
continue
}
if text == "" {
continue
}
if sb.Len() > 0 {
sb.WriteString(separator)
}
remaining := maxTotalBytes - sb.Len()
if remaining <= 0 {
break
}
if len(text) > remaining {
text = truncateOnRune(text, remaining)
}
sb.WriteString(text)
if sb.Len() >= maxTotalBytes {
break
}
}
if failures > 0 {
log.Printf("federalsignal: skipped %d unreadable attachment(s) of %d on one record", failures, len(urls))
}
return sb.String(), nil
}
// fetchOne downloads a single URL (appending the api_key only for a SAM host), optionally retains the
// raw bytes, and returns the extracted text. It is the only place the key touches a URL, and it returns
// errors that never embed the key bearing URL.
func (f *Fetcher) fetchOne(ctx context.Context, raw string) (string, error) {
parsed, err := url.Parse(raw)
if err != nil {
return "", errors.New("federalsignal: invalid attachment url")
}
reqURL := raw
if f.isSAMHost(parsed.Host) {
// Append the key to the existing query. Build it from the parsed URL so the key is never string
// concatenated into a malformed URL, and so it is added once even if the URL already has a query.
q := parsed.Query()
q.Set("api_key", f.key)
parsed.RawQuery = q.Encode()
reqURL = parsed.String()
}
body, err := f.download(ctx, reqURL)
if err != nil {
return "", err // already key safe, see download
}
if f.store != nil {
// Retain under a deterministic key derived from the ORIGINAL url (without the key), so retention is
// idempotent and the key never lands in the object key. A storage failure is not fatal to mining.
if perr := f.store.Put(ctx, rawKey(f.storePref, raw), body); perr != nil {
log.Printf("federalsignal: retain attachment failed: %v", perr)
}
}
// Name the extractor's dispatch off the URL path so the extension drives the format choice.
return docextract.ExtractText(ctx, parsed.Path, body)
}
// download GETs url with bounded retries on a transient failure and returns the body bytes capped at
// maxBodyBytes. Every error string is key safe: a *url.Error embeds the full request URL (which carries
// the api_key), so it is unwrapped to the transport cause which carries host:port at most.
func (f *Fetcher) download(ctx context.Context, target string) ([]byte, error) {
var lastErr error
for attempt := 0; attempt <= f.maxRetry; attempt++ {
if attempt > 0 {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(time.Duration(attempt) * backoffStep):
}
}
body, err := f.attempt(ctx, target)
if err == nil {
return body, nil
}
lastErr = err
}
return nil, lastErr
}
func (f *Fetcher) attempt(ctx context.Context, target string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
// A build error would echo the full URL incl. api_key, keep it generic.
return nil, errors.New("federalsignal: failed to build request")
}
resp, err := f.http.Do(req)
if err != nil {
// *url.Error.Error() embeds the full request URL incl. ?api_key=... NEVER wrap it whole: ue.Err is
// the transport cause and carries host:port at most, never the query string.
var ue *url.Error
if errors.As(err, &ue) {
return nil, fmt.Errorf("federalsignal: %s request failed: %v", ue.Op, ue.Err)
}
return nil, errors.New("federalsignal: request failed")
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
// Drain a small bounded snippet so the connection can be reused, but do not include it: a SAM error
// body can echo the request URL with the key.
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1024))
return nil, fmt.Errorf("federalsignal: status %d", resp.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodyBytes))
if err != nil {
return nil, errors.New("federalsignal: read body failed")
}
return body, nil
}
// isSAMHost reports whether host (which may include a port) is a registered SAM host.
func (f *Fetcher) isSAMHost(host string) bool {
_, ok := f.samHosts[strings.ToLower(host)]
return ok
}
// rawKey is the deterministic object key for a retained attachment: the prefix, then the lowercase hex
// sha256 of the original URL, with a .bin suffix. The api_key is [credential removed] part of it because the original
// URL (pre key) is hashed. Two runs of the same URL collide on one object.
func rawKey(prefix, originalURL string) string {
sum := sha256.Sum256([]byte(originalURL))
h := hex.EncodeToString(sum[:])
p := strings.Trim(prefix, "/")
if p == "" {
p = "federalsignal"
}
return p + "/attachments/" + h + ".bin"
}
// truncateOnRune cuts s to at most n bytes on a UTF-8 rune boundary so the cap never splits a rune.
func truncateOnRune(s string, n int) string {
if n >= len(s) {
return s
}
cut := n
for cut > 0 && s[cut]&0xC0 == 0x80 {
cut--
}
return s[:cut]
}