Documentation
¶
Overview ¶
Package shieldlabs is the ShieldLabs server SDK for Go.
A ShieldLabs integration has three steps:
- In the browser, the ShieldLabs agent runs an identification and hands your page a request ID. The browser never sees a Risk Score or a device ID.
- Your backend receives the request ID together with the protected action (signup, login, checkout) and reads the verdict for it from the History API, or receives the verdict in a signed webhook.
- Your backend decides what to do with the verdict: the Risk Score, the risk band, the detection flags and the identifiers.
This package covers step 2 and helps with step 3:
- NewClient creates a History API client (Private API Key, sec_...). IdentificationsService.Get reads the identification for one request ID, waiting until it has been scored. HistoryService.Search and HistoryService.All read the history of one device, user, visitor, IP address, session or cookie.
- NewManagementClient creates a Management API client (Secret Key plus registered domain) for ManagementClient.GetProfile.
- The webhook subpackage verifies X-Shield-Signature and parses webhook deliveries into typed events.
- RiskBand, IsRateLimited, EvaluateIdentification and UserHID are small, dependency-free helpers.
Webhook deliveries and History rows are normalized into one model, Identification, whose JSON field names follow the webhook contract.
Every client is safe for concurrent use by multiple goroutines. Create one client per key and reuse it.
Errors can be inspected with errors.Is and errors.As: API responses with an error status are returned as *APIError and match sentinels such as ErrAuthentication or ErrRateLimited; invalid arguments match ErrValidation and are reported before any request is sent.
Index ¶
- Constants
- Variables
- func DetectionFlagNames() []string
- func IsRateLimited(score int) bool
- func IsSentinelUserHID(userHID string) bool
- func SignalSlug(description string) string
- func UserHID(userID, secret string) (string, error)
- type APIError
- type Band
- type Client
- type ConnectionType
- type DetectionFlags
- type DomainProfile
- type EvaluateOptions
- type Evaluation
- type GetIdentificationOptions
- type HistoryIterateOptions
- type HistoryPage
- type HistorySearchOptions
- type HistoryService
- type IPInfo
- type Identification
- type IdentificationsService
- type LookupType
- type ManagementClient
- type Option
- type Reason
- type Signal
- type Source
- type TrafficSource
Examples ¶
Constants ¶
const ( FlagVPN = "vpn" FlagPrivacyRelay = "privacy_relay" FlagBrowserVPNProxy = "browser_vpn_proxy" FlagTor = "tor" FlagProxy = "proxy" FlagDatacenterIP = "datacenter_ip" FlagAbuser = "abuser" FlagOSMismatch = "os_mismatch" FlagOSNotDetected = "os_not_detected" FlagTimezoneMismatch = "timezone_mismatch" FlagAntiDetectBrowser = "anti_detect_browser" FlagBrowserAutomation = "browser_automation" FlagIPMismatch = "ip_mismatch" FlagIncognito = "incognito" FlagSearchBot = "search_bot" FlagSuspiciousPaidClick = "suspicious_paid_click" FlagJavaScriptDisabled = "javascript_disabled" FlagStunNotChecked = "stun_not_checked" FlagCheckIncomplete = "check_incomplete" )
Detection flag names as they appear on the wire. Use them with DetectionFlags.Get and EvaluateOptions.BlockFlags.
const ( // DefaultBaseURL is the origin of the History API. Request paths start // with /api/v1/history. DefaultBaseURL = "https://account.shieldlabs.ai" // DefaultManagementBaseURL is the origin of the Management API. Request // paths start with /v1. DefaultManagementBaseURL = "https://api.shieldlabs.ai" // NilUUID is the all-zero UUID. As a device ID it means that the // identification had no usable device signals; ban marker rows also use it // for other identifiers. NilUUID = "00000000-0000-0000-0000-000000000000" )
const ( SignalTor = "tor" SignalJavaScriptDisabled = "javascript_disabled" SignalOSMismatch = "os_mismatch" SignalAntidetectBrowser = "antidetect_browser" SignalProxyRoutedAntidetect = "proxy_routed_antidetect" SignalPortScanRoutedViaProxy = "port_scan_routed_via_proxy" SignalBrowserAutomation = "browser_automation" SignalStunNotChecked = "stun_not_checked" SignalStunLateCorrection = "stun_late_correction" SignalOSNotDetected = "os_not_detected" SignalBrowserVPNProxy = "browser_vpn_proxy" SignalVPN = "vpn" SignalPrivacyRelay = "privacy_relay" SignalProxy = "proxy" SignalDatacenterIP = "datacenter_ip" SignalAbuser = "abuser" SignalTimezoneMismatch = "timezone_mismatch" SignalRateLimited = "rate_limited" )
Known risk signal names (Signal.Name). The set is open: new signals can appear at any time, so treat names as strings and never as a closed enum.
const DefaultMaxAge = 5 * time.Minute
DefaultMaxAge is the default freshness window of EvaluateIdentification.
const Version = "1.0.0"
Version is the version of this module.
Variables ¶
var ( // ErrValidation reports an invalid argument. Nothing was sent. ErrValidation = errors.New("shieldlabs: invalid argument") // ErrBadRequest matches HTTP 400. ErrBadRequest = errors.New("shieldlabs: bad request") // ErrAuthentication matches HTTP 401 and 403: a missing, wrong or // disabled key, or a domain that does not match the key. ErrAuthentication = errors.New("shieldlabs: authentication failed") // ErrQuotaExceeded matches HTTP 402: the account has no requests left. ErrQuotaExceeded = errors.New("shieldlabs: quota exceeded") // ErrNotFound matches HTTP 404: an unknown route. An identification that // has not been found is not an error (see IdentificationsService.Get). ErrNotFound = errors.New("shieldlabs: not found") // ErrRateLimited matches HTTP 429. APIError.RetryAfter holds the // Retry-After delay when the server sent one. ErrRateLimited = errors.New("shieldlabs: rate limited") // ErrServer matches HTTP 5xx. ErrServer = errors.New("shieldlabs: server error") // ErrConnection reports a network failure: the request did not reach // ShieldLabs or the connection broke before a full response arrived. ErrConnection = errors.New("shieldlabs: connection failed") // ErrTimeout reports that an HTTP attempt ran out of time without a // response. ErrTimeout = errors.New("shieldlabs: request timed out") )
Sentinel errors. Match them with errors.Is:
if errors.Is(err, shieldlabs.ErrRateLimited) { ... }
ErrBadRequest, ErrAuthentication, ErrQuotaExceeded, ErrNotFound, ErrRateLimited and ErrServer match an *APIError with the corresponding HTTP status. ErrConnection and ErrTimeout wrap network failures, and ErrValidation reports an invalid argument detected before any request was sent.
Functions ¶
func DetectionFlagNames ¶
func DetectionFlagNames() []string
DetectionFlagNames returns the 19 detection flag names in wire order.
func IsRateLimited ¶
IsRateLimited reports whether a Risk Score is the rate-limit marker (a value above 100, in practice 999). ShieldLabs writes one such identification when a visitor IP goes over the identification rate limit; it carries no real verdict. Request IDs issued while that IP is blocked get no identification at all, so IdentificationsService.Get returns nil for them: treat them as unverified.
func IsSentinelUserHID ¶
IsSentinelUserHID reports whether a User HID value names no account: the empty string, "anonymous" (anonymous checks) and the sentinel values "fail", "-1" and "unknown". The comparison is exact and case-sensitive.
Example ¶
package main
import (
"fmt"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
for _, hid := range []string{"9f86d081884c7d659a2feaa0c55ad015", "anonymous", "unknown"} {
fmt.Println(hid, shieldlabs.IsSentinelUserHID(hid))
}
}
Output: 9f86d081884c7d659a2feaa0c55ad015 false anonymous true unknown true
func SignalSlug ¶
SignalSlug returns the signal name that ShieldLabs derives from a signal description. History rows carry descriptions (score_details) while webhooks carry names; the History client uses this function so that both surfaces report the same names.
Known descriptions map to fixed names. Any other description is slugified: the text before the first "(" is lowercased, spaces, "-" and "/" become "_", "≠" becomes "_neq_" and other punctuation is dropped. An empty result becomes "unknown".
Example ¶
package main
import (
"fmt"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
fmt.Println(shieldlabs.SignalSlug("Is VPN"))
fmt.Println(shieldlabs.SignalSlug("Antidetect browser (turn_block)"))
}
Output: vpn antidetect_browser
func UserHID ¶
UserHID derives a stable, irreversible User HID from your own user ID: the lowercase hex HMAC-SHA256 of userID keyed with secret (64 characters). Compute it on your server with a secret that never reaches the browser, then pass the result to the browser agent instead of a raw email address or account ID.
It returns an error matching ErrValidation when userID or secret is empty.
Example ¶
package main
import (
"fmt"
"log"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
hid, err := shieldlabs.UserHID("user-42", "server-side-secret")
if err != nil {
log.Fatal(err)
}
fmt.Println(hid)
}
Output: 7900fe533a0c4a3231be8bf94641b2f0b20bd2af0ef7bf2eadb759233cc3d822
Types ¶
type APIError ¶
type APIError struct {
// StatusCode is the HTTP status code.
StatusCode int
// Message is the error message parsed from the body: the "error" field of
// a JSON object, a bare JSON string, or a short plain-text body. It is ""
// when the body carries no message.
Message string
// Body is the raw response body.
Body []byte
// Header holds the response headers.
Header http.Header
// RetryAfter is the delay the Retry-After header asks for, given in
// seconds or as an HTTP date. It is 0 when the header is absent or
// invalid, and when it asks for no delay: 0 or a date in the past.
RetryAfter time.Duration
// contains filtered or unexported fields
}
APIError is returned when a ShieldLabs API answers with an error status, or with a response body the SDK cannot read. It matches the sentinel for its status code with errors.Is.
type Band ¶
type Band string
Band is a risk band derived from a Risk Score. Bands are computed on the client; they are not part of the wire format.
const ( // BandTrusted covers scores 0 to 29. BandTrusted Band = "trusted" // BandSuspicious covers scores 30 to 59. BandSuspicious Band = "suspicious" // BandDangerous covers scores 60 to 100. BandDangerous Band = "dangerous" // BandRateLimited marks a score above 100 (999): the rate-limit marker, // which is not a score. BandRateLimited Band = "rate_limited" )
Risk bands.
func RiskBand ¶
RiskBand returns the band of a Risk Score: trusted (0-29), suspicious (30-59) or dangerous (60-100). Scores above 100 are the rate-limit marker and return BandRateLimited.
Example ¶
package main
import (
"fmt"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
for _, score := range []int{0, 30, 60, 999} {
fmt.Println(score, shieldlabs.RiskBand(score))
}
}
Output: 0 trusted 30 suspicious 60 dangerous 999 rate_limited
type Client ¶
type Client struct {
// Identifications reads one identification by its request ID.
Identifications *IdentificationsService
// History searches identifications by device, user, visitor, IP address,
// session, cookie or request ID.
History *HistoryService
// contains filtered or unexported fields
}
Client reads identifications from the History API with a Private API Key (sec_...). Each Private API Key reads the identifications of one domain.
A Client is safe for concurrent use by multiple goroutines. Create it once and reuse it.
func NewClient ¶
NewClient returns a History API client for the given Private API Key.
It returns an error matching ErrValidation when the key is empty or contains whitespace or control characters (surrounding whitespace is trimmed), or when an option is invalid. A key that does not look like a Private API Key (sec_xxxxxxxx-xxxxxxxx-xxxxxxxx) only produces a warning on the configured logger, because the key format may change.
Defaults: base URL DefaultBaseURL, a 10-second timeout per HTTP attempt and 2 retries.
Example ¶
package main
import (
"log"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
client, err := shieldlabs.NewClient("sec_your_private_key")
if err != nil {
log.Fatal(err)
}
_ = client
}
Output:
type ConnectionType ¶
type ConnectionType string
ConnectionType is the connection class of an identification. The set is open: values other than the constants below are kept as they arrive.
const ( ConnectionTypeDirect ConnectionType = "direct" ConnectionTypeMobile ConnectionType = "mobile" ConnectionTypeVPN ConnectionType = "vpn" ConnectionTypeProxy ConnectionType = "proxy" ConnectionTypeTor ConnectionType = "tor" ConnectionTypePrivacyRelay ConnectionType = "privacy_relay" ConnectionTypeBrowserVPNProxy ConnectionType = "browser_vpn_proxy" ConnectionTypeUnknown ConnectionType = "unknown" )
Known connection types.
type DetectionFlags ¶
type DetectionFlags struct {
VPN bool `json:"vpn"`
PrivacyRelay bool `json:"privacy_relay"`
BrowserVPNProxy bool `json:"browser_vpn_proxy"`
Tor bool `json:"tor"`
Proxy bool `json:"proxy"`
DatacenterIP bool `json:"datacenter_ip"`
Abuser bool `json:"abuser"`
OSMismatch bool `json:"os_mismatch"`
OSNotDetected bool `json:"os_not_detected"`
TimezoneMismatch bool `json:"timezone_mismatch"`
AntiDetectBrowser bool `json:"anti_detect_browser"`
BrowserAutomation bool `json:"browser_automation"`
IPMismatch bool `json:"ip_mismatch"`
Incognito bool `json:"incognito"`
SearchBot bool `json:"search_bot"`
SuspiciousPaidClick bool `json:"suspicious_paid_click"`
JavaScriptDisabled bool `json:"javascript_disabled"`
StunNotChecked bool `json:"stun_not_checked"`
CheckIncomplete bool `json:"check_incomplete"`
}
DetectionFlags holds the 19 stable detection booleans of an identification. Flags missing from a delivery are false.
func (DetectionFlags) Get ¶
func (f DetectionFlags) Get(name string) (value, ok bool)
Get returns the flag with the given wire name, for example FlagBrowserAutomation. ok is false for an unknown name.
func (DetectionFlags) Map ¶
func (f DetectionFlags) Map() map[string]bool
Map returns the 19 flags keyed by their wire names.
type DomainProfile ¶
type DomainProfile struct {
// Domain is the registered domain.
Domain string `json:"domain"`
// RemainingIdentifications is the number of included identifications left
// on the account. It can be negative when the account is over its
// included volume.
RemainingIdentifications int64 `json:"remaining_identifications"`
// PublicKeyMasked is the public key with every character except the last
// 4 replaced by "*".
PublicKeyMasked string `json:"public_key_masked"`
// SecretKeyMasked is the Secret Key, masked the same way.
SecretKeyMasked string `json:"secret_key_masked"`
// CreatedAt is when the domain was registered (UTC), or the zero time
// when unknown.
CreatedAt time.Time `json:"created_at"`
// Raw is the original response object, including fields this model does
// not cover. Numbers are json.Number values.
Raw map[string]any `json:"-"`
}
DomainProfile describes a registered domain.
type EvaluateOptions ¶
type EvaluateOptions struct {
// MaxAge is the freshness window measured from ObservedAt. Zero means
// DefaultMaxAge (5 minutes); a negative value disables the check.
MaxAge time.Duration
// Now is the current time. The zero value means time.Now().
Now time.Time
// BlockBands lists the bands to refuse. Nil means [BandDangerous]; an
// empty, non-nil slice refuses no band.
BlockBands []Band
// BlockFlags lists detection flag names to refuse, checked in order. Nil
// means FlagBrowserAutomation and FlagJavaScriptDisabled; an empty,
// non-nil slice refuses no flag. Unknown names are ignored.
BlockFlags []string
// IsReplay reports whether a request ID was used before. The SDK stores
// nothing itself: record each request ID in your own store (atomically)
// and return true when it was already there. Return true as well when the
// store cannot be reached, so that failures refuse rather than allow.
IsReplay func(requestID string) bool
}
EvaluateOptions tunes EvaluateIdentification. The zero value applies the defaults, which are a starting point to adjust for your traffic.
type Evaluation ¶
type Evaluation struct {
// OK is true when no check refused the identification.
OK bool `json:"ok"`
// Reason is the first check that refused it, or "" when OK.
Reason Reason `json:"reason"`
// Band is the risk band of the identification, or "" when it is missing.
Band Band `json:"band"`
// Flag is the blocking flag when Reason is ReasonBlockedFlag.
Flag string `json:"flag,omitempty"`
}
Evaluation is the result of EvaluateIdentification.
func EvaluateIdentification ¶
func EvaluateIdentification(ident *Identification, opts EvaluateOptions) Evaluation
EvaluateIdentification applies a reusable guard policy to an identification before a protected action such as a signup. It checks, in this order:
- missing: ident is nil (never treat a missing identification as clean);
- replayed: opts.IsReplay reports the request ID as already used;
- stale: the identification is older than opts.MaxAge;
- rate_limited: the Risk Score is the rate-limit marker;
- no_device_signals: the device ID is the nil UUID;
- blocked_flag: a flag in opts.BlockFlags is set;
- blocked_band: the risk band is in opts.BlockBands.
It never performs I/O.
Example ¶
package main
import (
"fmt"
"time"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
now := time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC)
ident := &shieldlabs.Identification{
RequestID: "3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53",
DeviceID: "ac7c303d-971b-41d1-8e25-cd5b46b46aed",
RiskScore: 65,
ObservedAt: now.Add(-20 * time.Second),
}
used := map[string]bool{}
verdict := shieldlabs.EvaluateIdentification(ident, shieldlabs.EvaluateOptions{
Now: now,
IsReplay: func(requestID string) bool {
seen := used[requestID]
used[requestID] = true
return seen
},
})
fmt.Println(verdict.OK, verdict.Reason, verdict.Band)
verdict = shieldlabs.EvaluateIdentification(nil, shieldlabs.EvaluateOptions{})
fmt.Println(verdict.OK, verdict.Reason)
}
Output: false blocked_band dangerous false missing
type GetIdentificationOptions ¶
type GetIdentificationOptions struct {
// NoWait makes a single lookup instead of waiting for the verdict. That
// lookup is retried like any History request, and Timeout and
// PollInterval do not apply.
NoWait bool
// Timeout is the total time budget of the wait. Zero means 10 seconds.
// The last lookup runs at the deadline and gets at least one second to
// answer (or the client timeout when that is shorter), so Get can return
// up to one second after Timeout.
Timeout time.Duration
// PollInterval sets the waits between lookups: for an interval p they are
// p, 2p, 4p, 6p and then 8p, each capped at 2 seconds or at p when p is
// longer. The default 250 ms gives 250 ms, 500 ms, 1 s, 1.5 s, then 2 s;
// 1 s gives 1 s, then 2 s; 3 s gives a lookup every 3 s. Zero means
// 250 ms. Values below 100 ms are raised to 100 ms, a guard against unit
// mistakes such as PollInterval: 250, which is 250 ns.
PollInterval time.Duration
}
GetIdentificationOptions controls IdentificationsService.Get. The zero value, like a nil *GetIdentificationOptions, waits for the verdict with the defaults.
type HistoryIterateOptions ¶
type HistoryIterateOptions struct {
// PageSize is the number of rows requested per page, 1 to 100. Zero means
// 100.
PageSize int
// MaxItems stops the iteration after this many identifications. Zero
// means no limit.
MaxItems int
}
HistoryIterateOptions controls HistoryService.All.
type HistoryPage ¶
type HistoryPage struct {
// Data holds the identifications of this page.
Data []*Identification `json:"data"`
// Total is the number of rows matching the search.
Total int64 `json:"total"`
}
HistoryPage is one page of History results, newest first.
type HistorySearchOptions ¶
type HistorySearchOptions struct {
// Limit is the page size, 1 to 100. Zero means the default of 20.
Limit int
// Offset is the number of rows to skip, 0 or more.
Offset int
}
HistorySearchOptions controls one History page.
type HistoryService ¶
type HistoryService struct {
// contains filtered or unexported fields
}
HistoryService searches the History API. Get it from Client.History.
func (*HistoryService) All ¶
func (s *HistoryService) All(ctx context.Context, lookup LookupType, value string, opts *HistoryIterateOptions) iter.Seq2[*Identification, error]
All returns an iterator over every identification whose lookup identifier equals value, newest first, fetching pages of opts.PageSize rows as it goes. It stops after the last row (as reported by the page total), at an empty page, after opts.MaxItems identifications, or when the loop breaks.
Rows can shift between pages while new identifications arrive, so the iterator skips identifications it has already yielded (same request ID).
When a request fails, or the arguments are invalid, the iterator yields a nil identification with the error and stops:
for ident, err := range client.History.All(ctx, shieldlabs.LookupDeviceID, deviceID, nil) {
if err != nil {
return err
}
fmt.Println(ident.RequestID, ident.RiskScore)
}
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
client, err := shieldlabs.NewClient("sec_your_private_key")
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
for ident, err := range client.History.All(ctx, shieldlabs.LookupUserHID, "9f86d081884c7d659a2feaa0c55ad015",
&shieldlabs.HistoryIterateOptions{MaxItems: 500}) {
if err != nil {
log.Fatal(err)
}
fmt.Println(ident.ObservedAt.Format(time.RFC3339), ident.DeviceID, ident.PublicIP.Country)
}
}
Output:
func (*HistoryService) Search ¶
func (s *HistoryService) Search(ctx context.Context, lookup LookupType, value string, opts *HistorySearchOptions) (*HistoryPage, error)
Search returns one page of identifications whose lookup identifier equals value, newest first. Use it for account-abuse checks, for example to count how many accounts one device ID has signed up.
Arguments are validated on the client before anything is sent, so that a typo never turns into an unfiltered or failing query: the lookup type must be one of the seven Lookup constants, UUID values must be UUIDs (sent lowercase), an IP must be a dotted IPv4 address, Limit must be 0 to 100 and Offset must not be negative. A User HID is matched exactly; it must be non-empty, valid UTF-8 text without "/" and other than "." and "..", because the History API cannot match such values in the request path. Invalid arguments return an error matching ErrValidation.
Example ¶
package main
import (
"context"
"fmt"
"log"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
client, err := shieldlabs.NewClient("sec_your_private_key")
if err != nil {
log.Fatal(err)
}
// How many identifications, and how many distinct accounts, has this
// device? Skip the nil device ID (no usable device signals): it matches
// unrelated identifications.
deviceID := "ac7c303d-971b-41d1-8e25-cd5b46b46aed"
if deviceID == shieldlabs.NilUUID {
return
}
page, err := client.History.Search(context.Background(), shieldlabs.LookupDeviceID, deviceID,
&shieldlabs.HistorySearchOptions{Limit: 100})
if err != nil {
log.Fatal(err)
}
accounts := map[string]bool{}
for _, ident := range page.Data {
// AccountUserHID skips nil and the values that name no account
// ("anonymous", "fail", "-1", "unknown").
if hid, ok := ident.AccountUserHID(); ok {
accounts[hid] = true
}
}
fmt.Printf("%d identifications, %d accounts on this device\n", page.Total, len(accounts))
}
Output:
type IPInfo ¶
type IPInfo struct {
// IP is a dotted IPv4 address, or "" when none is known (for example for
// visitors on IPv6).
IP string `json:"ip"`
// Country is the English country name, for example "Germany", or ""
// when unknown.
Country string `json:"country"`
}
IPInfo is an IP address and its country.
type Identification ¶
type Identification struct {
// RequestID is the UUID the browser agent returned for this
// identification. Use it to join the browser call, the webhook and the
// History row, and to make sure one identification authorizes only one
// protected action.
RequestID string `json:"request_id"`
// VisitorID is the server-side visitor identifier. It is sticky to the
// device: a new cookie on a known device usually keeps the visitor ID.
VisitorID string `json:"visitor_id"`
// DeviceID is the server-side device identifier. It survives cleared
// cookies and private windows. [NilUUID] means that the identification
// had no usable device signals.
DeviceID string `json:"device_id"`
// SessionID identifies one visit on one origin.
SessionID string `json:"session_id"`
// CookieID is the first-party browser identifier kept by the agent.
CookieID string `json:"cookie_id"`
// UserHID is the hashed or pseudonymous account ID your page passed to the
// agent. It is nil when none was stored. Anonymous checks use the value
// "anonymous"; "fail", "-1" and "unknown" are other values that name no
// account. Use [Identification.AccountUserHID] to skip all of them, for
// example when counting the accounts of one device.
UserHID *string `json:"user_hid"`
// Domain is the registered domain the identification belongs to.
Domain string `json:"domain"`
// PublicIP is the IP address of the HTTP request and its country.
PublicIP IPInfo `json:"public_ip"`
// LocalIP is the local network address observed during the network
// checks, and its country.
LocalIP IPInfo `json:"local_ip"`
// ConnectionType is the connection class, for example
// [ConnectionTypeVPN]. Unknown values are kept.
ConnectionType ConnectionType `json:"connection_type"`
// OS is the operating system, for example "Windows" or "Mac OS X".
OS string `json:"os"`
// Browser is the browser, for example "Chrome" or "Safari".
Browser string `json:"browser"`
// DeviceType is "desktop", "mobile", "tablet" or "unknown".
DeviceType string `json:"device_type"`
// TrafficSource describes where the visit came from.
TrafficSource TrafficSource `json:"traffic_source"`
// RiskScore is an integer from 0 to 100. A value above 100 (999) is a
// rate-limit marker, never a score: see [IsRateLimited]. Use [RiskBand]
// for the band.
RiskScore int `json:"risk_score"`
// Signals lists the weighted risk signals behind the score, in the order
// ShieldLabs reported them. Weights can be negative and names can repeat.
// Use signals for display and logging, and branch on DetectionFlags and
// RiskScore instead. Never add up the weights yourself.
Signals []Signal `json:"signals"`
// DetectionFlags holds the 19 stable detection booleans.
DetectionFlags DetectionFlags `json:"detection_flags"`
// ObservedAt is when the identification was observed, in UTC. For a
// webhook it is the time the verdict was sent; for a History row it is
// the time the identification was stored. It is the zero time when the
// value could not be read.
ObservedAt time.Time `json:"observed_at"`
// Source tells whether the identification came from a webhook or from
// the History API.
Source Source `json:"source"`
// Raw is the original webhook data object or History row, including
// fields this model does not cover. Numbers are json.Number values so
// that integers keep their exact value.
Raw map[string]any `json:"-"`
}
Identification is one run of the ShieldLabs agent in one browser: one billable request and one History row. Webhook deliveries and History API rows are both normalized into this model, and its JSON field names follow the webhook contract.
Every field is always set. Unknown values (a new connection type, a new risk signal) are kept as they arrive.
func ParseHistoryRow ¶
func ParseHistoryRow(row []byte) (*Identification, error)
ParseHistoryRow normalizes one row of a History API response (a JSON object) into an Identification. The History client already does this for you; the function is exported for rows obtained another way.
func ParseWebhookData ¶
func ParseWebhookData(data []byte) (*Identification, error)
ParseWebhookData normalizes the data object of an identification.scored webhook event into an Identification. It does not verify a signature: use the webhook package to verify and parse a delivery in one step.
func (*Identification) AccountUserHID ¶
func (i *Identification) AccountUserHID() (userHID string, ok bool)
AccountUserHID returns the User HID when it names one of your accounts. ok is false when UserHID is nil or one of the values that name no account (see IsSentinelUserHID). Use it when you count the accounts seen on one device, visitor or IP address:
accounts := map[string]bool{}
for _, ident := range page.Data {
if hid, ok := ident.AccountUserHID(); ok {
accounts[hid] = true
}
}
func (*Identification) Band ¶
func (i *Identification) Band() Band
Band returns the risk band of the identification's Risk Score.
func (*Identification) IsRateLimited ¶
func (i *Identification) IsRateLimited() bool
IsRateLimited reports whether the Risk Score is the rate-limit marker.
type IdentificationsService ¶
type IdentificationsService struct {
// contains filtered or unexported fields
}
IdentificationsService reads single identifications. Get it from Client.Identifications.
func (*IdentificationsService) Get ¶
func (s *IdentificationsService) Get(ctx context.Context, requestID string, opts *GetIdentificationOptions) (*Identification, error)
Get returns the identification for a request ID, or (nil, nil) when none was found in time.
Scoring is asynchronous: the History row appears about 1 to 3 seconds after the browser call, and follow-up checks can refine it for up to about 10 seconds. Start the identification in the browser when the user begins the action (for example when the signup form opens), so that the verdict is usually ready when your backend asks for it.
Unless opts.NoWait is set, Get waits for the row, and opts.Timeout (10 s by default) is the total budget of the wait. Get looks up the request ID at once, then again after waits of 250 ms, 500 ms, 1 s, 1.5 s and then every 2 s (p, 2p, 4p, 6p and then 8p for a PollInterval p, each capped at max(2 s, p)), and the last lookup runs at the deadline. Each lookup is a single HTTP attempt, without the client's retries, and gets min(client timeout, max(time left, 1 s)) to answer.
A 429 or 5xx answer, a network failure or an attempt timeout does not end the wait: the next lookup follows on the same schedule. The History API allows about 15 requests per second per domain for all callers together, so after a 429 the next wait is the longest of the scheduled wait, one second and the Retry-After delay capped at 10 seconds. Without the header, and with "Retry-After: 0" or a date in the past, the delay counts as 0 and the one-second minimum still applies. Near the deadline that wait is shortened like any other, so that the last lookup runs at the deadline, but when the capped Retry-After delay alone would end after the deadline, Get returns the 429 error at once.
A 400, 401, 403 or 404 answer ends the wait at once with its error (ErrBadRequest, ErrAuthentication, ErrNotFound), because a wrong key or a wrong base URL does not heal, and so does any other answer that another lookup cannot change.
When the time is up, Get returns the error of the last lookup if it failed, and (nil, nil) otherwise. A nil result means "unverified", never "clean": treat it like a failed check. Get returns the first version of the row it finds; for the refined state, read the row again later with HistoryService.Search and LookupRequestID.
With opts.NoWait, Get makes a single lookup, retried like any History request.
The request ID must be a UUID; otherwise the error matches ErrValidation and nothing is sent.
Example ¶
package main
import (
"context"
"fmt"
"log"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
client, err := shieldlabs.NewClient("sec_your_private_key")
if err != nil {
log.Fatal(err)
}
// requestID comes from your page, sent together with the protected action.
requestID := "3f2b8c1e-9d4a-4e6b-8a7c-2d1e0f9b6a53"
ident, err := client.Identifications.Get(context.Background(), requestID, nil)
if err != nil {
log.Fatal(err)
}
if ident == nil {
fmt.Println("not scored in time: treat as unverified")
return
}
fmt.Println(ident.RiskScore, ident.Band(), ident.DetectionFlags.VPN)
}
Output:
type LookupType ¶
type LookupType string
LookupType selects the identifier a History search matches on.
const ( // LookupIP matches the public IP address, written as a dotted IPv4 // address. LookupIP LookupType = "ip" // LookupUserHID matches the User HID exactly (case-sensitive). LookupUserHID LookupType = "user_hid" // LookupVisitorID matches the visitor ID (UUID). LookupVisitorID LookupType = "visitor_id" // LookupRequestID matches the request ID (UUID). LookupRequestID LookupType = "request_id" // LookupDeviceID matches the device ID (UUID). LookupDeviceID LookupType = "device_id" // LookupSessionID matches the session ID (UUID). LookupSessionID LookupType = "session_id" // LookupCookieID matches the cookie ID (UUID). LookupCookieID LookupType = "cookie_id" )
The seven History lookup types.
type ManagementClient ¶
type ManagementClient struct {
// contains filtered or unexported fields
}
ManagementClient calls the Management API with the Secret Key of one registered domain.
The Management API allows about 15 requests per minute per caller IP address and then blocks that IP for 10 minutes. Call it sparingly (cache the profile) and never in a loop. The client never retries a 429 answer, because retrying would only run into the block.
A ManagementClient is safe for concurrent use by multiple goroutines.
func NewManagementClient ¶
func NewManagementClient(secretKey, domain string, opts ...Option) (*ManagementClient, error)
NewManagementClient returns a Management API client for a Secret Key and the domain it was issued for.
The domain is normalized before it is sent, because the API matches the registered domain exactly: surrounding spaces, a scheme, a path, a trailing slash and a leading "www." are removed and the rest is lowercased, so "https://www.Example.com/" becomes "example.com".
It returns an error matching ErrValidation when the secret key or the domain is empty or contains whitespace or control characters (surrounding whitespace is trimmed), or when an option is invalid. Defaults: base URL DefaultManagementBaseURL, a 10-second timeout per HTTP attempt and 2 retries (never on 429).
func (*ManagementClient) GetProfile ¶
func (m *ManagementClient) GetProfile(ctx context.Context) (*DomainProfile, error)
GetProfile returns the profile of the client's domain (GET /v1/profile). An exhausted account answers with an error matching ErrQuotaExceeded, a wrong secret or domain with ErrAuthentication and the per-IP limit with ErrRateLimited.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"log"
shieldlabs "github.com/ShieldLabs-ai/shieldlabs-go"
)
func main() {
mgmt, err := shieldlabs.NewManagementClient("your_secret_key", "example.com")
if err != nil {
log.Fatal(err)
}
// About 15 requests per minute per IP: cache the result.
profile, err := mgmt.GetProfile(context.Background())
if errors.Is(err, shieldlabs.ErrRateLimited) {
log.Fatal("Management API limit reached: wait before calling again")
}
if err != nil {
log.Fatal(err)
}
fmt.Println(profile.Domain, profile.RemainingIdentifications)
}
Output:
type Option ¶
type Option func(*config)
Option configures a Client or a ManagementClient.
func WithBaseURL ¶
WithBaseURL overrides the API origin, for example "https://dev.account.shieldlabs.ai" for the History API or a local test server. A trailing slash is removed. For the History client a trailing "/api" is removed too, because request paths already start with /api.
An empty value keeps the default, so an optional override can be passed straight from the environment:
shieldlabs.WithBaseURL(os.Getenv("SHIELDLABS_API_BASE_URL"))
The URL must use https. Plain http is accepted for localhost, 127.0.0.0/8 and [::1], and for other hosts only together with WithInsecureHTTP.
func WithHTTPClient ¶
WithHTTPClient sets the *http.Client used for requests, for example to configure a proxy or a custom transport. The default is a new http.Client that uses http.DefaultTransport.
func WithInsecureHTTP ¶
func WithInsecureHTTP() Option
WithInsecureHTTP accepts a plain http base URL on a host other than localhost, 127.0.0.0/8 or [::1], for example a mock server in a container network. Keys then travel unencrypted, so never use it in production; the client logs a warning when it is created with such a URL.
func WithLogger ¶
WithLogger sets the logger used for configuration warnings, for example a Private API Key with an unexpected format. The default is slog.Default(). The SDK never logs keys, secrets or response bodies.
func WithMaxRetries ¶
WithMaxRetries sets how many times a failed request is retried (default 2). Zero disables retries.
func WithTimeout ¶
WithTimeout sets the timeout of each HTTP attempt (default 10 seconds). Zero disables the per-attempt timeout; the request context still applies.
type Reason ¶
type Reason string
Reason explains why EvaluateIdentification refused an identification.
const ( // ReasonMissing: no identification was found. Treat it as unverified, // never as clean. ReasonMissing Reason = "missing" // ReasonReplayed: the IsReplay callback reported the request ID as // already used. ReasonReplayed Reason = "replayed" // ReasonStale: the identification is older than MaxAge, or its time is // unknown. ReasonStale Reason = "stale" // ReasonRateLimited: the Risk Score is the rate-limit marker. ReasonRateLimited Reason = "rate_limited" // ReasonNoDeviceSignals: the device ID is the nil UUID. ReasonNoDeviceSignals Reason = "no_device_signals" // ReasonBlockedFlag: a flag listed in BlockFlags is set (see // Evaluation.Flag). ReasonBlockedFlag Reason = "blocked_flag" // ReasonBlockedBand: the risk band is listed in BlockBands. ReasonBlockedBand Reason = "blocked_band" )
Refusal reasons, in the order EvaluateIdentification checks them.
type Signal ¶
type Signal struct {
// Name is the signal slug, for example [SignalProxy]. The set is open.
Name string `json:"name"`
// Weight is the signal's contribution. It can be negative (corrections).
Weight int `json:"weight"`
// Description is the human-readable description from a History row, or
// nil for webhook signals. It is free text: never branch on it.
Description *string `json:"description"`
}
Signal is one weighted risk signal behind a Risk Score.
type TrafficSource ¶
type TrafficSource struct {
// Channel is the traffic channel, for example "Google Ads",
// "Organic Search", "Referral" or "Direct".
Channel string `json:"channel"`
// ReferrerDomain is the referring site without "www.", or the crawler
// name for search engine crawlers.
ReferrerDomain string `json:"referrer_domain"`
// LandingURL is the landing page URL without its fragment. It can contain
// query string values.
LandingURL string `json:"landing_url"`
// ClickIDType is the ad click ID parameter found on the landing URL, for
// example "gclid".
ClickIDType string `json:"click_id_type"`
UTMSource string `json:"utm_source"`
UTMMedium string `json:"utm_medium"`
UTMCampaign string `json:"utm_campaign"`
UTMContent string `json:"utm_content"`
UTMTerm string `json:"utm_term"`
}
TrafficSource describes where a visit came from. Every field is "" when unknown.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
nethttp
command
Command nethttp is a minimal net/http server that shows both halves of a ShieldLabs integration:
|
Command nethttp is a minimal net/http server that shows both halves of a ShieldLabs integration: |
|
internal
|
|
|
wire
Package wire holds the low-level parsing rules shared by the shieldlabs packages: whitespace trimming, timestamp formats and JSON decoding.
|
Package wire holds the low-level parsing rules shared by the shieldlabs packages: whitespace trimming, timestamp formats and JSON decoding. |
|
Package webhook verifies and parses ShieldLabs webhook deliveries.
|
Package webhook verifies and parses ShieldLabs webhook deliveries. |