mirror of
https://github.com/hanzoai/idv.git
synced 2026-10-30 18:33:39 +00:00
LexisNexis reported "approved" out of InitiateVerification for any non-4xx reply and again from CheckStatus with no request at all; Intellicheck issued its request and then ignored the response, including its status code, so a 500 and a rejected document both read as approved. None of the three read what the provider decided. A caller gating on any of them would admit an unverified subject. An integration that does not parse the response now refuses instead of answering, and the refusal names the provider so an operator can tell it apart from a network fault. LexisNexis loses the query builder nothing called; implementing it means parsing the FlexID verdict and mapping it to a status. Two properties are now table-driven over every constructible provider, so one added later inherits both: a freshly created verification is never already approved, and an unread status read refuses. The tests point at a server that ANSWERS — an unreachable base URL would pass for the wrong reason, leaving the synthesized approval downstream of a return no test reaches. scripts/mutants.py drives cloud's strict-scored engine over both properties and the webhook guards: 6 rows, 6 KILLED on a green baseline. LLM.md now states per provider whether it can read a verdict and whether it can prove a webhook, and carries the retirement runbook — sequenced on the route, since an orphaned Ingress publishes idv.hanzo.ai today despite its CR disabling ingress. Co-authored-by: Hanzo Dev <[email protected]>
90 lines
3.1 KiB
Go
90 lines
3.1 KiB
Go
// Package intellicheck implements the IDV provider for Intellicheck document verification.
|
|
package provider
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
IntellicheckProdURL = "https://idn-platform-api.intellicheck.com"
|
|
IntellicheckSandboxURL = "https://idn-platform-api-uat.intellicheck.com"
|
|
)
|
|
|
|
type IntellicheckConfig struct {
|
|
BaseURL string
|
|
CompanyToken string
|
|
SubscriptionKey string
|
|
}
|
|
|
|
type Intellicheck struct {
|
|
cfg IntellicheckConfig
|
|
client *http.Client
|
|
}
|
|
|
|
func NewIntellicheck(cfg IntellicheckConfig) *Intellicheck {
|
|
if cfg.BaseURL == "" {
|
|
cfg.BaseURL = IntellicheckSandboxURL
|
|
}
|
|
return &Intellicheck{cfg: cfg, client: &http.Client{Timeout: 30 * time.Second}}
|
|
}
|
|
|
|
func (p *Intellicheck) Name() string { return ProviderIntellicheck }
|
|
|
|
func (p *Intellicheck) InitiateVerification(ctx context.Context, user *VerificationRequest) (*VerificationResponse, error) {
|
|
body := map[string]any{"location": "US", "purpose": "Age Verification"}
|
|
payload, _ := json.Marshal(body)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/ingest", bytes.NewReader(payload))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Ocp-Apim-Subscription-Key", p.cfg.SubscriptionKey)
|
|
req.Header.Set("Authorization", "Bearer "+p.cfg.CompanyToken)
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("intellicheck: %d", resp.StatusCode)
|
|
}
|
|
var result struct {
|
|
UserToken string `json:"userToken"`
|
|
IngestToken string `json:"ingestToken"`
|
|
}
|
|
json.Unmarshal(respBody, &result)
|
|
return &VerificationResponse{VerificationID: result.UserToken, Provider: ProviderIntellicheck, Status: StatusPending}, nil
|
|
}
|
|
|
|
func (p *Intellicheck) CheckStatus(ctx context.Context, sessionID string) (*VerificationStatusResult, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", p.cfg.BaseURL+"/results/"+sessionID, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Ocp-Apim-Subscription-Key", p.cfg.SubscriptionKey)
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
// The request is well-formed and the response is never read: this returned
|
|
// "approved" for every outcome, including a 500 and including a document the
|
|
// provider rejected. A provider that cannot read a verdict must not report one,
|
|
// so this refuses until the results body is parsed.
|
|
return nil, fmt.Errorf("intellicheck: the results response is not parsed — no decision can be reported")
|
|
}
|
|
|
|
// ParseWebhook refuses every payload: no Intellicheck callback signature scheme
|
|
// is implemented here, so a payload arriving on this path proves nothing about
|
|
// who sent it. CheckStatus is the authenticated path to a verdict.
|
|
func (p *Intellicheck) ParseWebhook(body []byte, headers map[string]string) (*WebhookEvent, error) {
|
|
return nil, fmt.Errorf("intellicheck webhook verification is not implemented; poll CheckStatus for the verdict: %w", ErrWebhookUnsigned)
|
|
}
|