signalgate

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

signalgate — Go backend SDK

Thin HTTP forwarder for SignalGate antifraud. Tenants embed this in their backend to call /v0/check (synchronous verdict) and /v0/log (async analytics). The SDK does no crypto — the opaque encrypted payload comes from @signalgate/frontend-js-sdk.

Install

go get github.com/SignalGate/signalgate-go

Quickstart

Check() and Log() are two calls at two points in your funnel, not two ways to send one event:

  • Check()before the action you're gating. Synchronous; returns a verdict so you can block or allow. The server records the check for analytics.
  • Log()after the action completes. Fire-and-forget telemetry; no verdict.

A typical flow calls both:

package main

import (
    "errors"
    "log"
    "net/http"
    "time"

    "github.com/SignalGate/signalgate-go"
)

var client *signalgate.Client

func main() {
    c, err := signalgate.New(
        "pk_live_...",  // API key minted by the SignalGate dashboard
    )
    if err != nil {
        log.Fatal(err)
    }
    client = c
    defer client.Close(5 * time.Second)

    // ... wire handleCheckout into your router
}

// handleCheckout runs at the checkout step of your funnel — fill in
// chargeTheCustomer and doneEvent with your own application logic.
func handleCheckout(r *http.Request) error {
    // 1. BEFORE the gated action — get a verdict and gate on it.
    v := 2
    gateEvent := signalgate.Event{
        UserID:    "u_123",
        IP:        r.Header.Get("X-Forwarded-For"),
        Method:    "checkout",
        Timestamp: "2026-04-01T13:08:50+00:00",
        Payload: signalgate.EncryptedPayload{
            Encrypted: "...",
            Timestamp: 1748102400000,
            Nonce:     "...",
            V:         &v,
        },
        Custom: map[string]any{"plan": "pro"},
    }
    verdict, err := client.Check(gateEvent)
    if err != nil {
        log.Printf("check error: %v", err)
        // fail-open: continue; details in client.Metrics()
    }
    if verdict.Action == "block" {
        return errors.New("blocked by SignalGate")
    }

    chargeTheCustomer()  // your paid action

    // 2. AFTER the action completes — log it (fire-and-forget analytics).
    client.Log(doneEvent(r))  // a fresh event captured at this funnel point
    return nil
}

You can also call Log() on its own for actions you aren't gating — background jobs, downstream action completions, page-view telemetry, etc.

Distinct events per call. Check and Log fire at different moments, so each carries its own event with its own fingerprint payload (its own nonce). Don't forward the byte-identical payload to both back-to-back — the server's nonce-freshness guard would replay-reject the second.

At process shutdown:

client.Close(5 * time.Second)  // drains the log queue within the deadline

Or use it with deferred cleanup:

client, _ := signalgate.New("pk_live_...")
defer client.Close(5 * time.Second)
// ... use client

Configuration

Override the defaults selectively via functional options:

client, err := signalgate.New(
    "pk_live_...",
    signalgate.WithCheckTimeoutMS(3000),
    signalgate.WithLogTimeoutMS(1000),
    signalgate.WithLogQueueCapacity(10000),
    signalgate.WithLogMaxRetries(3),
    signalgate.WithLogRetryBaseMS(200),
    signalgate.WithFailOpen(true),  // default: on timeout/5xx, Check() returns allow
)
Option Default Meaning
WithCheckTimeoutMS 3000 per-request timeout for Check()
WithLogTimeoutMS 1000 per-request timeout for each log delivery attempt
WithLogQueueCapacity 10000 bounded queue size for the log worker
WithLogMaxRetries 3 retries after the first attempt (⇒ 4 attempts max)
WithLogRetryBaseMS 200 base for exponential backoff
WithFailOpen true on transient error Check() returns a synthesized allow

Error handling

Check() raises on 4xx (tenant bug — bad/revoked pk_* API key, malformed event). With fail-open on (the default), timeouts / network errors / 5xx / an unparseable response return a synthesized CheckResult{Action: "allow", FailedOpen: true} and a nil error.

import (
    "errors"
    "log"

    "github.com/SignalGate/signalgate-go"
)

verdict, err := client.Check(event)
var se *signalgate.ServerError
if errors.As(err, &se) {
    // se.StatusCode, se.Code, se.Message, se.RequestID, se.Details
    log.Printf("server error: [%d] %s", se.StatusCode, se.Message)
}

Error types:

  • ConfigError — bad constructor config, or client already closed.
  • TimeoutError — request deadline exceeded.
  • NetworkError — DNS/TCP/TLS/I-O before a response.
  • ServerError — non-2xx response carrying the error envelope.

Log() never raises. Failures are counted in client.Metrics().

Metrics

client.Metrics().Get("check_total", nil)
client.Metrics().Get("check_failed_open_total", nil)
client.Metrics().Get("log_dropped_total", map[string]string{"reason": "queue_full"})
snapshot := client.Metrics().Snapshot()  // all counters

Full counter list:

Counter Labels Incremented when
check_total Check() called
check_success_total Check() returned a real verdict
check_failed_open_total Check() returned a synthesized allow
check_error_total type Check() hit an error condition — raised, or failed open
log_enqueued_total Log() accepted into queue
log_sent_total server acknowledged a log event
log_http_error_total status a log delivery attempt failed (status = HTTP code, or network)
log_dropped_total reason queue_full / closed / retry_exhausted

License

Licensed under the Apache License, Version 2.0.

Documentation

Overview

Package signalgate forwards encrypted fingerprint events from your backend to the SignalGate antifraud service.

There are two calls, made at two points in your funnel. Check is synchronous and returns a verdict you can gate on, so you call it before the action you are protecting. Log is fire-and-forget telemetry, so you call it after the action completes. A typical flow uses both.

client, err := signalgate.New("pk_live_...")
if err != nil {
	log.Fatal(err)
}
defer client.Close(5 * time.Second)

verdict, err := client.Check(event)
if err != nil {
	log.Printf("check error: %v", err)
}
if verdict.Action == "block" {
	return errors.New("blocked by SignalGate")
}

By default the client fails open: if a check times out or the service returns a 5xx, Check returns a synthesized allow with FailedOpen set rather than an error, so an outage cannot block your traffic. See WithFailOpen to change it.

This SDK performs no cryptography. The opaque encrypted payload comes from the SignalGate frontend SDK and is forwarded verbatim. The package has no third-party dependencies.

Full documentation: https://signalgate.ai/docs/go

Index

Constants

View Source
const (
	DefaultCheckTimeoutMS   = 3000
	DefaultLogTimeoutMS     = 1000
	DefaultLogQueueCapacity = 10000
	DefaultLogMaxRetries    = 3
	DefaultLogRetryBaseMS   = 200
	DefaultFailOpen         = true
)

Default configuration values, identical across every SignalGate SDK.

View Source
const (
	SDKName    = "signalgate-backend-sdk"
	Version    = "0.1.0"
	APIVersion = "v0"
)

Identifying constants for this SDK. Version is this package's own release line; the User-Agent embeds it.

Variables

This section is empty.

Functions

func UserAgent

func UserAgent() string

UserAgent returns the canonical User-Agent for this port:

signalgate-backend-sdk/<version> (go/<go_version>; <os>)

The server groups traffic by the leading token, so the format is fixed. The API key is never embedded here.

Types

type CheckResult

type CheckResult struct {
	Action           string  `json:"action"`
	Score            float64 `json:"score"`
	RequestID        string  `json:"request_id"`
	TenantID         string  `json:"tenant_id"`
	Timestamp        string  `json:"timestamp"`
	ProcessingTimeUS int64   `json:"processing_time_us"`
	FailedOpen       bool    `json:"-"`
}

CheckResult is the immutable verdict returned by Check. FailedOpen is a local-only flag, never read from the wire; it is true only for a synthesized fail-open allow, which lets you tell a real allow from an outage.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the SDK facade. It is safe for concurrent use by multiple goroutines: every field is set once in New and never mutated afterward, except the closed flag, which is itself concurrency-safe.

Construct one with New and share it; the zero Client is not usable. Call Close at shutdown to drain queued Log events.

func New

func New(apiKey string, opts ...Option) (*Client, error)

New constructs a Client. apiKey must be non-empty; every timeout and retry count must be non-negative; the log queue capacity must be greater than zero. Invalid configuration returns a *ConfigError. New starts exactly one background log-worker goroutine, so pair it with Close.

func (*Client) Check

func (c *Client) Check(event Event) (CheckResult, error)

Check is the synchronous antifraud gate. Call it before the action you are gating and act on the returned verdict.

With fail-open enabled (the default), a timeout, network failure, 5xx, or unparseable response returns a synthesized allow with FailedOpen set and a nil error, so a SignalGate outage cannot block your traffic. A 4xx always propagates as a *ServerError, since it means the request itself was wrong — a bad or revoked API key, or a malformed event. Check does not retry.

func (*Client) Close

func (c *Client) Close(deadline time.Duration) error

Close shuts the client down within deadline, draining the log worker and counting anything left behind as log_dropped_total{reason="closed"}. It is idempotent, and closes the transport only if the client owns it (see WithTransport). A zero deadline defaults to five times the log timeout.

func (*Client) Log

func (c *Client) Log(event Event)

Log is the fire-and-forget analytics path. Call it after the action completes. It enqueues and returns immediately: it never blocks and never returns an error, so delivery failures surface only in Metrics, under log_dropped_total and log_http_error_total.

func (*Client) Metrics

func (c *Client) Metrics() *Metrics

Metrics returns the client's counters, for reading via Get or Snapshot.

type ConfigError

type ConfigError struct {
	Message string
}

ConfigError is returned from New for invalid configuration, and by Check when the client is already closed.

func (*ConfigError) Error

func (e *ConfigError) Error() string

type EncryptedPayload

type EncryptedPayload struct {
	Encrypted string `json:"encrypted"`
	Timestamp int64  `json:"timestamp"`
	Nonce     string `json:"nonce"`
	V         *int   `json:"v,omitempty"`
}

EncryptedPayload is the opaque, immutable fingerprint blob produced by the SignalGate frontend SDK. This SDK forwards it verbatim and never inspects or decrypts it. V is the outer payload version; when nil it is omitted from the wire entirely, never serialized as "v": null.

type Error

type Error interface {
	error
	// contains filtered or unexported methods
}

Error is the sealed root of the SDK error taxonomy. Every error this SDK surfaces satisfies it, so callers can distinguish SDK errors from any other error in a chain. The unexported marker method keeps the hierarchy closed to this package, which means a type switch over the four concrete types below is exhaustive and stays exhaustive.

type Event

type Event struct {
	UserID    string           `json:"user_id"`
	IP        string           `json:"ip"`
	Method    string           `json:"method"`
	Timestamp string           `json:"timestamp"`
	Payload   EncryptedPayload `json:"payload"`
	Custom    map[string]any   `json:"custom,omitempty"`
}

Event is the immutable input value for both Check and Log. Custom is an arbitrary object omitted from the wire when nil, never serialized as "custom": null.

Check and Log fire at different points in your funnel, so each call takes its own Event carrying its own freshly captured payload. Do not forward one byte-identical payload to both: the service's nonce-freshness guard rejects the second as a replay.

type HTTPResponse

type HTTPResponse struct {
	StatusCode int
	Body       map[string]any
	RequestID  string
}

HTTPResponse is a parsed 2xx response. Body is the decoded JSON object (JSON numbers decode to float64, exactly as encoding/json would produce), matching what the real transport returns so fakes and production are interchangeable.

type HTTPTransport

type HTTPTransport struct {
	// contains filtered or unexported fields
}

HTTPTransport is the production transport, wrapping a single pooled HTTP client. It redacts Authorization in every log line.

func NewHTTPTransport

func NewHTTPTransport(apiKey, userAgent string, logger Logger) *HTTPTransport

NewHTTPTransport builds the production transport.

func (*HTTPTransport) Close

func (t *HTTPTransport) Close() error

Close releases the pooled connections.

func (*HTTPTransport) Post

func (t *HTTPTransport) Post(url string, body map[string]any, opts PostOptions) (HTTPResponse, error)

Post is the production POST path: one attempt, JSON UTF-8, the required headers, a per-request deadline, and error classification into TimeoutError/NetworkError/ServerError.

type Logger

type Logger interface {
	Debug(msg string, fields map[string]any)
	Info(msg string, fields map[string]any)
	Warn(msg string, fields map[string]any)
	Error(msg string, fields map[string]any)
}

Logger is the structured logging seam. Implementations receive a message and a bag of fields. The default is NoopLogger.

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics holds thread-safe labeled counters. A counter is identified by its name plus the set of (key, value) label pairs; label ordering is irrelevant to identity. Increments are internal to the SDK; callers read via Get and Snapshot.

func (*Metrics) Get

func (m *Metrics) Get(name string, labels map[string]string) int64

Get returns the value of the named counter for the given label set (nil for an unlabeled counter). Label ordering does not affect the result.

func (*Metrics) Snapshot

func (m *Metrics) Snapshot() map[string]int64

Snapshot returns a flat, Prometheus-style view of all counters (labels sorted, e.g. `log_dropped_total{reason="queue_full"}`), so callers can diff two snapshots for a metrics-delta loop.

type NetworkError

type NetworkError struct {
	Message string
}

NetworkError is returned on DNS/TCP/TLS/I-O failures before a server response.

func (*NetworkError) Error

func (e *NetworkError) Error() string

type NoopLogger

type NoopLogger struct{}

NoopLogger discards everything. It is the default logger.

func (NoopLogger) Debug

func (NoopLogger) Debug(msg string, fields map[string]any)

func (NoopLogger) Error

func (NoopLogger) Error(msg string, fields map[string]any)

func (NoopLogger) Info

func (NoopLogger) Info(msg string, fields map[string]any)

func (NoopLogger) Warn

func (NoopLogger) Warn(msg string, fields map[string]any)

type Option

type Option func(*clientOptions)

Option configures a Client at construction. See the With* functions.

func WithCheckTimeoutMS

func WithCheckTimeoutMS(ms int) Option

WithCheckTimeoutMS sets the per-request timeout for Check (default 3000).

func WithFailOpen

func WithFailOpen(v bool) Option

WithFailOpen toggles fail-open for Check (default true).

func WithLogMaxRetries

func WithLogMaxRetries(n int) Option

WithLogMaxRetries sets retries after the first log attempt (default 3 ⇒ 4 attempts).

func WithLogQueueCapacity

func WithLogQueueCapacity(n int) Option

WithLogQueueCapacity sets the bounded log-queue size (default 10000; must be > 0).

func WithLogRetryBaseMS

func WithLogRetryBaseMS(ms int) Option

WithLogRetryBaseMS sets the exponential backoff base in ms (default 200).

func WithLogTimeoutMS

func WithLogTimeoutMS(ms int) Option

WithLogTimeoutMS sets the per-attempt timeout for each log delivery (default 1000).

func WithLogger

func WithLogger(l Logger) Option

WithLogger injects a structured Logger (default NoopLogger).

func WithTransport

func WithTransport(t Transport) Option

WithTransport injects a Transport, chiefly so tests can substitute a fake for the network. When set, the Client does not own the transport and will not Close it.

type PostOptions

type PostOptions struct {
	TimeoutMS      int
	RequestID      string
	IdempotencyKey string
}

PostOptions carries the per-attempt HTTP knobs the Client hands the transport: the per-request timeout, the fresh X-Request-Id for this attempt, and the Idempotency-Key that stays stable across retries of one call.

type ServerError

type ServerError struct {
	StatusCode int
	Code       string
	Message    string
	RequestID  string
	Details    map[string]any
}

ServerError is returned on a non-2xx response carrying the service's error envelope. Its message renders as "[<status_code>] <code>: <message>".

func (*ServerError) Error

func (e *ServerError) Error() string

type TimeoutError

type TimeoutError struct {
	Message string
}

TimeoutError is returned when a request exceeds its deadline.

func (*TimeoutError) Error

func (e *TimeoutError) Error() string

type Transport

type Transport interface {
	Post(url string, body map[string]any, opts PostOptions) (HTTPResponse, error)
	Close() error
}

Transport is the injectable HTTP seam, primarily so tests can substitute a fake for the network. Post performs one POST attempt: on a non-2xx it returns a *ServerError; on a deadline it returns a *TimeoutError; on a pre-response failure it returns a *NetworkError. See WithTransport.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL