Documentation
¶
Overview ¶
Package audit is a polyglot client SDK for tamper-evident, SQL-queryable audit logging — the Go port of @smooai/audit. It provides a canonical event schema, canonical JSON serialization, a per-org-per-day SHA-256 hash chain, and an emit client, all verified byte-for-byte against a shared parity corpus.
Index ¶
- Constants
- func CanonicalJSON(v any) (string, error)
- func ComputeEventHash(event AuditEvent) (string, error)
- func IsNamespacedAction(action string) bool
- type AuditActorType
- type AuditClient
- type AuditDiff
- type AuditEvent
- type AuditOutcome
- type AuditResource
- type ChainVerification
- type VerifyFailureCode
Constants ¶
const ( DefaultMaxRetries = 3 DefaultRetryBackoff = 100 * time.Millisecond )
Default retry behaviour, shared with every other language SDK. The numbers live in spec/parity-corpus.json's retryPolicy and are asserted there, so they cannot drift apart across the five implementations.
const ( ActionUserSignin = "user.signin" ActionUserSignout = "user.signout" ActionUserPasswordChanged = "user.password_changed" ActionUserInvited = "user.invited" ActionOrgCreated = "org.created" ActionOrgMemberAdded = "org.member_added" ActionOrgMemberRemoved = "org.member_removed" ActionOrgRoleChanged = "org.role_changed" ActionOrgSubscriptionChgd = "org.subscription_changed" ActionOrgProductPurchased = "org.product_purchased" ActionAgentConfigChanged = "agent.config_changed" ActionAgentKnowledgeAdded = "agent.knowledge_doc_added" ActionAgentKnowledgeRemvd = "agent.knowledge_doc_removed" ActionAgentEscalationMade = "agent.escalation_created" ActionAgentToolFailed = "agent.tool_failed" ActionCRMContactCreated = "crm.contact_created" ActionCRMContactMerged = "crm.contact_merged" ActionCRMContactDeleted = "crm.contact_deleted" ActionAPIKeyMinted = "api.key_minted" ActionAPIKeyRotated = "api.key_rotated" ActionAPIKeyRevoked = "api.key_revoked" ActionIntegrationConnected = "integration.connected" ActionIntegrationDisconn = "integration.disconnected" )
Baseline event actions — the generic surface every app shares. Emitters are NOT limited to these: any consumer defines its own namespaced actions ("billing.invoice_voided", "fieldops.task_submitted", …) and emits them directly; canonicalization treats Action as an opaque string. These baseline names stay stable because dashboards/alerts/reports pivot off them.
const Version = "0.4.1"
Version is the current version of the smooai-audit Go package.
Variables ¶
This section is empty.
Functions ¶
func CanonicalJSON ¶
CanonicalJSON serializes a decoded JSON value — exactly the value domain json.Decoder with UseNumber produces (nil, bool, json.Number, string, []any, map[string]any) — to canonical JSON, byte-for-byte identical to the TS canonicalJsonStringify and the Rust canonical_json.
Contract (must NOT drift across languages):
- primitives → JSON.stringify semantics (see writeJSString / numbers)
- arrays → "[" + items.join(",") + "]" (order PRESERVED)
- objects → keys SORTED by UTF-16 code-unit order at every depth, each rendered "key":value joined by ","; no insignificant whitespace.
A JSON null that is *present* in the value (e.g. diff.after on a delete) renders as "null" — it is NOT omitted. Absent optional fields never reach here: the emitter drops them (omitempty) before the value is built, mirroring the TS "value is undefined → key omitted" rule.
func ComputeEventHash ¶
func ComputeEventHash(event AuditEvent) (string, error)
ComputeEventHash returns the lowercase hex SHA-256 of an event's canonical JSON. The event is hashed WITHOUT its own HashCurrent (empty → omitted by json tag) and WITH HashPrevious already set to the chain head (or nil on the first event of a day) — the same input the TS computeEventHash feeds to the canonical serializer.
func IsNamespacedAction ¶
IsNamespacedAction validates the "namespace.verb" action convention (a lowercase namespace and at least one lowercase verb segment, dot-separated, e.g. "crm.contact_created", "google.gmail.message_sent"). Assert this at your trust boundary; canonicalization itself treats Action as an opaque string, so this is a convention check, not a hard schema constraint.
Types ¶
type AuditActorType ¶
type AuditActorType string
AuditActorType identifies who performed an action.
const ( ActorUser AuditActorType = "user" ActorAgent AuditActorType = "agent" ActorSystem AuditActorType = "system" ActorIntegration AuditActorType = "integration" ActorAPIClient AuditActorType = "api_client" )
type AuditClient ¶
type AuditClient struct {
// Endpoint is the audit ingest URL events are POSTed to.
Endpoint string
// Token is the bearer token sent as "Authorization: Bearer <token>".
// Optional — omitted when empty.
Token string
// HTTPClient overrides the HTTP client. Defaults to http.DefaultClient.
HTTPClient *http.Client
// MaxRetries is the total number of attempts on a transient failure
// (transport error or HTTP 5xx). Zero means DefaultMaxRetries.
MaxRetries int
// RetryBackoff is the base backoff, doubled on each retry. Zero means
// DefaultRetryBackoff.
RetryBackoff time.Duration
}
AuditClient emits audit events to a configurable ingest endpoint over HTTP. Transport is stdlib net/http; the only dependency is the OpenTelemetry trace API (no SDK), used to read the ambient span for envelope trace correlation.
func NewClient ¶
func NewClient(endpoint, token string) *AuditClient
NewClient returns an AuditClient bound to the given endpoint + token.
func (*AuditClient) Emit ¶
func (c *AuditClient) Emit(ctx context.Context, event AuditEvent) error
Emit seals the event into the hash chain (computes and attaches HashCurrent) and POSTs the canonical JSON envelope to the ingest endpoint with the bearer token:
{"event":{…the sealed event…},"spanId":"…","traceId":"…"}
The bytes under "event" are the exact preimage-plus-hash the verifier replays, so every store agrees byte-for-byte; the trace ids ride outside them.
Transient failures (transport errors and HTTP 5xx) are retried with exponential backoff; a 4xx is returned immediately, since it will say the same thing on the next attempt. An audit event that silently fails to emit is a hole in the record, so the error is always returned — never swallowed. Emit is synchronous by design: `go client.Emit(ctx, event)` is how Go does async, and ctx cancellation is honoured both in-flight and between retries.
type AuditDiff ¶
AuditDiff is a structural diff captured at write time. Either side may be absent (omitted on create/delete); a *present* null (e.g. After on a delete) is meaningful and is serialized as JSON null, never dropped.
type AuditEvent ¶
type AuditEvent struct {
ID string `json:"id"`
OrganizationID string `json:"organizationId"`
ActorType AuditActorType `json:"actorType"`
ActorID string `json:"actorId"`
ActorEmail string `json:"actorEmail,omitempty"`
Action string `json:"action"`
Resource AuditResource `json:"resource"`
Outcome AuditOutcome `json:"outcome"`
Reason string `json:"reason,omitempty"`
SessionID string `json:"sessionId,omitempty"`
ConversationID string `json:"conversationId,omitempty"`
IPAddress string `json:"ipAddress,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
GeoCountry string `json:"geoCountry,omitempty"`
Diff *AuditDiff `json:"diff,omitempty"`
Metadata map[string]any `json:"metadata"`
Timestamp string `json:"timestamp"`
// HashPrevious links this event to the prior one in its per-org-per-day
// chain. Absent (nil) on the first event of a chain — not null.
HashPrevious *string `json:"hashPrevious,omitempty"`
// HashCurrent is SHA-256 of canonical-JSON(this event minus HashCurrent).
// Empty (thus omitted) while computing the hash; set afterward.
HashCurrent string `json:"hashCurrent,omitempty"`
}
AuditEvent is the canonical audit event — the shared shape every language SDK serializes. It carries ZERO customer content by design: only identity, resource references, outcome, and namespaced action + metadata the emitter supplies. Field names and json tags are part of the cross-language hash contract and are verified byte-for-byte against ../spec/parity-corpus.json.
Optional fields use omitempty so an absent field is dropped before canonicalization (matching the TS "undefined → omitted" rule). The event fed to ComputeEventHash is this struct with HashCurrent empty (thus omitted).
func BuildHashChain ¶
func BuildHashChain(events []AuditEvent, genesisHash string) ([]AuditEvent, error)
BuildHashChain folds events into a per-org-per-day chain, stamping each event with its HashPrevious (the prior event's HashCurrent) and HashCurrent. genesisHash seeds the chain ("" for a fresh chain → first event's HashPrevious is omitted, not null). Returns sealed copies; the input slice is not mutated.
type AuditOutcome ¶
type AuditOutcome string
AuditOutcome is the result of an action.
const ( OutcomeSuccess AuditOutcome = "success" OutcomeFailure AuditOutcome = "failure" OutcomeDenied AuditOutcome = "denied" )
type AuditResource ¶
AuditResource is the thing an action was performed against. The Type is a namespaced kind (e.g. "crm.contact"); the ID is its canonical identifier.
type ChainVerification ¶ added in v0.4.0
type ChainVerification struct {
OK bool
// BrokenAt is the index of the first event that failed.
BrokenAt int
// Code says why it failed.
Code VerifyFailureCode
}
ChainVerification is the verdict from VerifyChain. BrokenAt and Code are only meaningful when OK is false.
func VerifyChain ¶ added in v0.4.0
func VerifyChain(events []AuditEvent, genesisPreviousHash string) (ChainVerification, error)
VerifyChain verifies an ordered chain: it recomputes every HashCurrent and confirms each HashPrevious matches the prior event's HashCurrent.
genesisPreviousHash is the hash the FIRST event must link to — pass the chain head you already have when verifying a slice that continues an existing chain. Pass "" only when events starts at the true beginning of the chain (first event of the org's day), where HashPrevious must be nil.
What replay cannot see: removing events from the TAIL leaves a chain that still verifies — every remaining link is genuine. Detecting that needs an external anchor (a stored chain head, an expected count) compared against the last event's HashCurrent. OK means "nothing here was altered", not "nothing is missing"; the corpus pins this as an explicit fixture so the limit stays visible.
type VerifyFailureCode ¶ added in v0.4.0
type VerifyFailureCode string
VerifyFailureCode says why a chain failed to verify. These codes are the cross-language contract — every SDK returns the same code for the same corruption, asserted by chainFixtures in spec/parity-corpus.json.
const ( // HashPreviousMismatch: an event's HashPrevious is not the prior event's // HashCurrent — the LINK is wrong: a reorder, a deletion, a truncated head, // a rewritten link. HashPreviousMismatch VerifyFailureCode = "hash_previous_mismatch" // HashCurrentMismatch: the event's own content no longer hashes to its // stored HashCurrent — the event BODY was edited after sealing. HashCurrentMismatch VerifyFailureCode = "hash_current_mismatch" )