contract

package
v1.10.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package contract wires chronicle into the Forge dashboard's contract path. It registers the `chronicle` contributor, declares the intents the React dashboard reads, and answers them from chronicle's own domain packages.

scope.go is the security boundary. Every handler resolves the viewer's scope from the principal's claims before it touches the store, and every detail handler re-checks ownership after it fetches.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Register

func Register(
	d *dispatcher.Dispatcher,
	reg fcontract.Registry,
	wreg fcontract.WardenRegistry,
	deps Deps,
) error

Register loads the manifest, registers the contributor, and binds handlers.

func ValidateDefaultScope

func ValidateDefaultScope(appID, tenantID string) error

ValidateDefaultScope checks the configured default app and tenant, the values of chronicle.dashboard.app_id and chronicle.dashboard.tenant_id.

Nothing is trimmed. A value with a leading or trailing space, or with a control character in it, would be stored and compared exactly as written, so it would name a scope that looks like the real one on every page and matches none of its data. It is refused rather than quietly repaired, the same rule a policy category follows.

A tenant with no app is refused because the default tenant only ever applies to a session whose app came from the default app, so on its own it would do nothing while looking like it narrowed every session.

Types

type AggregateGroupDTO

type AggregateGroupDTO struct {
	Bucket   string `json:"bucket,omitempty"`
	Category string `json:"category,omitempty"`
	Action   string `json:"action,omitempty"`
	Outcome  string `json:"outcome,omitempty"`
	Severity string `json:"severity,omitempty"`
	Resource string `json:"resource,omitempty"`
	Count    int64  `json:"count"`
}

AggregateGroupDTO is one group of an aggregation result on the wire. The overview stats and the report sections reuse this type, so it is declared here once and must not be declared a second time.

type AggregateInput

type AggregateInput struct {
	After   string   `json:"after,omitempty"`
	Before  string   `json:"before,omitempty"`
	GroupBy []string `json:"groupBy"`
}

AggregateInput bounds an events.aggregate call. GroupBy is validated against audit.ResolveGroupBy before the store is ever touched, so a bad field is answered as the caller's mistake and not as a store failure.

type AggregateResponse

type AggregateResponse struct {
	Groups []AggregateGroupDTO `json:"groups"`
	Total  int64               `json:"total"`
}

AggregateResponse is the result of events.aggregate.

type ArchiveListInput

type ArchiveListInput struct {
	Limit  int `json:"limit,omitempty"`
	Offset int `json:"offset,omitempty"`
}

ArchiveListInput pages through the viewer's own scope's archive records.

type ArchiveListResponse

type ArchiveListResponse struct {
	Archives []ArchiveSummary `json:"archives"`
	HasMore  bool             `json:"hasMore"`
}

ArchiveListResponse is one page of archive records. There is no total: retention.Store has no archive count method, and counting a page would state the page size as the total. HasMore comes from fetching one row past the page.

type ArchiveSummary

type ArchiveSummary struct {
	ID            string `json:"id"`
	PolicyID      string `json:"policyId"`
	Category      string `json:"category"`
	EventCount    int64  `json:"eventCount"`
	FromTimestamp string `json:"fromTimestamp"`
	ToTimestamp   string `json:"toTimestamp"`
	SinkName      string `json:"sinkName"`
	SinkRef       string `json:"sinkRef,omitempty"`
	TenantID      string `json:"tenantId,omitempty"`
	CreatedAt     string `json:"createdAt"`
}

ArchiveSummary is one archived batch as the dashboard lists it.

type CheckpointListInput

type CheckpointListInput struct {
	StreamID string `json:"streamId,omitempty"`
	Limit    int    `json:"limit"`
	Offset   int    `json:"offset"`
}

CheckpointListInput pages through one chain's checkpoints.

StreamID empty means the viewer's own chain (its exact app and tenant). An app-wide viewer passes a tenant's chain ID, taken from streams.list, to list that tenant's checkpoints. A chain the viewer does not own answers NOT_FOUND.

type CheckpointListResponse

type CheckpointListResponse struct {
	Checkpoints []CheckpointSummary `json:"checkpoints"`
	HasMore     bool                `json:"hasMore"`

	// Supported distinguishes two different reasons a list can come back
	// empty. False means this deployment cannot answer checkpoint questions
	// at all: no store or signer configured (checkpointsAvailable), or a
	// backend that holds no checkpoints by design, such as redis, which
	// implements checkpoint.Store and answers every call with
	// checkpoint.ErrUnsupported. True with an empty Checkpoints means
	// checkpointing works here and this particular chain simply has none
	// yet. The page needs to tell an operator "this backend holds none"
	// apart from "none yet", and Supported is the only field that can.
	Supported bool `json:"supported"`
}

CheckpointListResponse is one page of one chain's checkpoints.

There is deliberately no Total field. checkpoint.ListOpts is {Limit, Offset} and checkpoint.Store has no count method, so a real total could only be produced by scanning every checkpoint the chain holds on every call to this intent. HasMore is computed instead, by asking the store for one row past the requested page and trimming it off below: that costs nothing beyond what the page already reads, unlike a count.

Checkpoints is never null on the wire. Every answer carries an array, empty when there is nothing to list, so a client can iterate it without a null check. That holds for the unsupported answers too, which is why Supported, not the array, says why it is empty.

type CheckpointResult

type CheckpointResult struct {
	ID      string `json:"id"`
	FromSeq uint64 `json:"fromSeq"`
	ToSeq   uint64 `json:"toSeq"`

	SignatureValid bool `json:"signatureValid"`

	HashMatch   bool `json:"hashMatch"`
	HashChecked bool `json:"hashChecked"`

	ContinuityOK      bool `json:"continuityOk"`
	ContinuityChecked bool `json:"continuityChecked"`

	Note string `json:"note,omitempty"`
}

CheckpointResult mirrors verify.CheckpointResult field for field. Each *Checked flag travels with its value because false alone cannot say whether the check ran.

type CheckpointSummary

type CheckpointSummary struct {
	ID         string `json:"id"`
	StreamID   string `json:"streamId"`
	FromSeq    uint64 `json:"fromSeq"`
	ToSeq      uint64 `json:"toSeq"`
	EventCount int64  `json:"eventCount"`
	CreatedAt  string `json:"createdAt"`
	SignKeyID  string `json:"signKeyId"`
}

CheckpointSummary is one signed checkpoint as the dashboard lists it.

It lives here rather than beside the checkpoints handlers because StreamSummary embeds it. The checkpoints group reuses this type and must not declare a second one.

StreamID names the chain the checkpoint was taken over, read off the signed record rather than inferred from the request, so the page can link straight to that chain's sequences. It discloses nothing new: every intent that returns a checkpoint has already confirmed the viewer owns its chain.

type CoverageSpan

type CoverageSpan struct {
	FromSeq uint64 `json:"fromSeq"`
	ToSeq   uint64 `json:"toSeq"`
	Level   string `json:"level"`
	Note    string `json:"note,omitempty"`
}

CoverageSpan is one graded span of a chain. It is a slice on the report and not a single level, because a chain that had HMAC turned on later is unkeyed below its pin and keyed above it, and flattening that into one level misdescribes the oldest events, which are the ones an investigation usually cares about.

type CustomReportSection

type CustomReportSection struct {
	Title      string   `json:"title"`
	Categories []string `json:"categories,omitempty"`
	Actions    []string `json:"actions,omitempty"`
	Severity   []string `json:"severity,omitempty"`
	Notes      string   `json:"notes,omitempty"`
}

CustomReportSection defines one section of a custom report. Empty categories, actions and severity match everything. The title is bounded by maxCustomSectionTitleLen, the notes by maxCustomSectionNotesLen, each filter list by maxCustomSectionFilters and each filter value by maxCustomFilterValueLen.

type DeletePolicyInput

type DeletePolicyInput struct {
	ID string `json:"id"`
}

DeletePolicyInput names the retention policy to delete.

type DeletePolicyResponse

type DeletePolicyResponse struct {
	ID string `json:"id"`
}

DeletePolicyResponse echoes the ID that was deleted.

type Deps

type Deps struct {
	Store     store.Store
	Chronicle *chronicle.Chronicle
	Engine    *compliance.Engine
	Enforcer  *retention.Enforcer
	Erasure   *erasure.Service

	// Checkpointer, CheckpointStore and CheckpointSigner are nil unless
	// checkpoints.enabled is true. They travel as a set: a checkpoint store
	// without a signer proves nothing, since anyone who could write the row
	// could write a fabricated one.
	Checkpointer     *checkpoint.Checkpointer
	CheckpointStore  checkpoint.Store
	CheckpointSigner checkpoint.Signer

	// HashChain is what verification recomputes digests under. A nil value
	// leaves an unkeyed chain, which cannot check an HMAC deployment at all,
	// so an HMAC deployment must pass its keyed chain here.
	HashChain *hash.Chain

	// MaxVerifySpan caps the number of sequences verify.run will walk in one
	// call. Zero means use maxVerifySpan, the package default of 100,000.
	//
	// VerifyChain holds every event in the resolved range in memory at once,
	// so an unbounded call on a long chain is an out-of-memory crash, and any
	// read-capable operator could trigger one by sending fromSeq 0, toSeq 0.
	// This exists to turn that into a refusal the UI can act on instead.
	MaxVerifySpan uint64

	Config SurfaceConfig

	// DefaultAppID is the app a session takes when its claims carry no
	// "app_id" at all. It is what lets a single-app deployment run before its
	// auth provider populates claims. A claim that is present but unusable
	// never falls back to it, and a session with no signed-in user never
	// takes it. Empty means every session must bring its own app.
	DefaultAppID string

	// DefaultTenantID is the tenant a session takes when its app came from
	// DefaultAppID, that is when its claims carry no "app_id" at all, and
	// carry neither "tenant_id" nor "org_id". A session whose app came from a
	// claim never takes it, even for the same app. It is meaningless without
	// DefaultAppID and Register refuses that combination. Empty leaves such a
	// session app-wide.
	DefaultTenantID string

	// Logger receives the underlying cause of every store error that reaches
	// the caller as CodeInternal. The contract error carries only a generic
	// message, so this log line is the one place the cause survives. Nil
	// means a no-op logger.
	Logger log.Logger
}

Deps is what the handlers need. Store is required; the rest reflect what this deployment configured, and a nil one means the intents that need it answer CodeUnavailable rather than panicking.

type EnforceResponse

type EnforceResponse struct {
	Archived   int64 `json:"archived"`
	Purged     int64 `json:"purged"`
	Retained   int64 `json:"retained"`
	MoreRemain bool  `json:"moreRemain"`
	Failed     bool  `json:"failed"`
}

EnforceResponse is the outcome of one retention.enforce pass.

Retained is always 0 today. It is copied from retention.EnforceResult, whose Retained counter the enforcer adds up across policies but no policy ever sets, so nothing counts events a policy chose to keep. The field stays so the wire does not change when the enforcer starts counting. Do not read it as "events kept by policy": that number is not measured. Purges that were recorded in the chain show up on verify.run as retained ranges, which is a different thing.

MoreRemain is true when eligible events are still there after the pass. One pass loads at most retention.DefaultPurgeBatchSize events per policy, so a large backlog takes several, and the library's own result has no way to say so.

Failed is true when a policy failed part-way. The counts are still the true counts of what was archived and purged before the failure, and that purge is permanent, which is why this is a successful response carrying a flag rather than an error that would throw the counts away.

type ErasureListInput

type ErasureListInput struct {
	Limit  int `json:"limit,omitempty"`
	Offset int `json:"offset,omitempty"`
}

ErasureListInput pages through the viewer's own scope's erasure records. There is deliberately no appId or tenantId field: scope comes from the principal, through scopeFromPrincipal, and never from the request.

type ErasureListResponse

type ErasureListResponse struct {
	Erasures []ErasureSummary `json:"erasures"`
	Total    int64            `json:"total"`
	HasMore  bool             `json:"hasMore"`
}

ErasureListResponse is one page of erasure records. Total comes from CountErasures, never from len(Erasures): that method exists precisely so a caller wanting a total does not have to count a bounded list, and the page caption would understate the real count on every page after the first if it did.

type ErasurePreviewInput

type ErasurePreviewInput struct {
	SubjectID string `json:"subjectId"`
}

ErasurePreviewInput names the subject a prospective erasure would target.

type ErasurePreviewResponse

type ErasurePreviewResponse struct {
	SubjectID      string `json:"subjectId"`
	EventsAffected int64  `json:"eventsAffected"`
}

ErasurePreviewResponse is the count a confirm dialog shows before an erasure command fires.

EventsAffected covers only the viewer's own scope: it comes from CountBySubject scoped to the caller's app and tenant, which is the most this contract is allowed to reveal about another tenant's data. A subject ID is not unique to one app or tenant, and an erasure is confined to the viewer's scope the same way, so this is the number erasures.request will report as eventsAffected.

type ErasureResult

type ErasureResult struct {
	ID                string `json:"id"`
	SubjectID         string `json:"subjectId"`
	EventsAffected    int64  `json:"eventsAffected"`
	KeyDestroyed      bool   `json:"keyDestroyed"`
	LegacyKeyRetained bool   `json:"legacyKeyRetained"`
}

ErasureResult is the outcome of erasures.request, for the page to report.

EventsAffected is how many of the subject's events in the viewer's own app and tenant were marked erased. Nothing outside that scope is counted or touched.

KeyDestroyed is true when the encryption key or keys those events were sealed under are gone, which is what makes the payload unrecoverable.

LegacyKeyRetained is true only alongside KeyDestroyed false. It means the events are marked erased and no read path shows their payload, but they were sealed before keys were scoped, under a key that live events in another app or tenant still depend on, so the key was kept. It goes the first time an erasure finds no other scope still using it, and running the erasure again later is how it is cleaned up. The page should say the erasure is not yet cryptographic when it sees this.

LegacyKeyRetained is always on the wire, false included: a page cannot tell "not retained" from "an older server that never said" if the field disappears when false.

type ErasureSummary

type ErasureSummary struct {
	ID                string `json:"id"`
	SubjectID         string `json:"subjectId"`
	Reason            string `json:"reason"`
	RequestedBy       string `json:"requestedBy"`
	EventsAffected    int64  `json:"eventsAffected"`
	KeyDestroyed      bool   `json:"keyDestroyed"`
	Status            string `json:"status"`
	LegacyKeyRetained bool   `json:"legacyKeyRetained"`
	CreatedAt         string `json:"createdAt"`
	// TenantID is empty for an app-wide erasure, which covers every tenant in
	// the app. Sent even when empty; see EventSummary.TenantID.
	TenantID string `json:"tenantId"`
}

ErasureSummary is one erasure record as the dashboard reads it: the evidence that a data subject's right to erasure was honoured.

Status is "pending" or "completed". A pending erasure did not finish: its events may be marked and some of its keys may be gone, but KeyDestroyed is false because not every key is confirmed destroyed. The page should say so and point the operator at running the erasure again, which finishes it under a new record. Records from before erasures had a status read as completed.

LegacyKeyRetained means the same as on ErasureResult. Both fields are always on the wire, false and "completed" included, for the same reason.

type EventDetail

type EventDetail struct {
	EventSummary
	StreamID   string         `json:"streamId"`
	Hash       string         `json:"hash"`
	PrevHash   string         `json:"prevHash"`
	HashScheme string         `json:"hashScheme,omitempty"`
	HashKeyID  string         `json:"hashKeyId,omitempty"`
	Reason     string         `json:"reason,omitempty"`
	SubjectID  string         `json:"subjectId,omitempty"`
	UserAgent  string         `json:"userAgent,omitempty"`
	RequestID  string         `json:"requestId,omitempty"`
	SessionID  string         `json:"sessionId,omitempty"`
	Metadata   map[string]any `json:"metadata,omitempty"`
	ErasedAt   string         `json:"erasedAt,omitempty"`
	ErasureID  string         `json:"erasureId,omitempty"`

	// EncryptionKeyID names the key the event's sealed fields were encrypted
	// under, empty when nothing was sealed. It is not covered by the event's
	// digest, so it is a claim the row makes and nothing verifies it: erasure
	// derives the keys it destroys from the row's scope, never from this.
	// Sent even when empty, for the same reason as TenantID.
	EncryptionKeyID string `json:"encryptionKeyId"`
}

EventDetail adds what the detail page needs: the payload, the chain position, and the scheme provenance that tells an operator which level THIS event was written under, which is not necessarily the level the chain is pinned to now.

A field on this type equal to crypto.ErasedMarker ("[ERASED]") is evidence of an erasure only when Erased is true and ErasureID names the erasure. The literal text is not reserved: an application can record it as an ordinary value, and an erased value can also arrive with Erased false and no ErasureID. So the marker alone proves nothing, and only Erased true with an ErasureID does. The event is readable either way: erasure replaces the sealed fields with the marker rather than making the row unreadable, and that substitution already happened upstream, on the store this Deps was configured with, before the event ever reached this handler.

type EventListInput

type EventListInput struct {
	After      string   `json:"after,omitempty"`
	Before     string   `json:"before,omitempty"`
	UserID     string   `json:"userId,omitempty"`
	SessionID  string   `json:"sessionId,omitempty"`
	RequestID  string   `json:"requestId,omitempty"`
	Categories []string `json:"categories,omitempty"`
	Actions    []string `json:"actions,omitempty"`
	Resources  []string `json:"resources,omitempty"`
	Severity   []string `json:"severity,omitempty"`
	Outcome    []string `json:"outcome,omitempty"`
	Limit      int      `json:"limit,omitempty"`
	Offset     int      `json:"offset,omitempty"`
	Order      string   `json:"order,omitempty"`
}

EventListInput is audit.Query's filter set on the wire. Every filter is multi-value except the time bounds, matching the Go type.

There is no appId or tenantId field, on purpose. Scope comes from the principal, through viewScope.applyQuery, and never from the request.

type EventListResponse

type EventListResponse struct {
	Events  []EventSummary `json:"events"`
	Total   int64          `json:"total"`
	HasMore bool           `json:"hasMore"`
}

EventListResponse is one page of the log. Total and HasMore always come from the store's own QueryResult, never from len(Events): the page caption reads "50 of 12,431 events", and counting the rows on screen would make that caption a lie on every page after the first.

type EventSummary

type EventSummary struct {
	ID         string `json:"id"`
	Timestamp  string `json:"timestamp"`
	Sequence   uint64 `json:"sequence"`
	Action     string `json:"action"`
	Resource   string `json:"resource"`
	ResourceID string `json:"resourceId,omitempty"`
	Category   string `json:"category"`
	Outcome    string `json:"outcome"`
	Severity   string `json:"severity"`
	UserID     string `json:"userId,omitempty"`
	IP         string `json:"ip,omitempty"`
	Erased     bool   `json:"erased"`

	// TenantID is empty for an app-level event. An app-wide viewer reads
	// every tenant's events and needs it to tell them apart. It is sent even
	// when empty: a server from before this field sends nothing, and a page
	// that read the missing field as "app level" would be making that up.
	TenantID string `json:"tenantId"`
}

EventSummary is one row of the log. Metadata is deliberately absent: it is freeform and can be large, and sending it for every row of a virtualised table would dominate the response. events.detail carries it.

type EventsByUserInput

type EventsByUserInput struct {
	UserID string `json:"userId"`
	After  string `json:"after,omitempty"`
	Before string `json:"before,omitempty"`
	Limit  int    `json:"limit,omitempty"`
	Offset int    `json:"offset,omitempty"`
}

EventsByUserInput requests one user's events within a time bound, scoped to the viewer's own app and tenant.

events.byUser is served by Store.Query with UserID set, not by Store.ByUser. ByUser's Total is only the length of the page it returns -- see store/sqlite/audit.go's ByUser and store/redis/audit.go's ByUser, both of which set Total: int64(len(events)) -- so it is not a count, and a caption built on it ("N of total") would state a false number the moment more rows exist than fit on the page. Query's Total is a real count on sqlite, postgres and mongo, each of which runs a separate count query alongside the page (store/sqlite/audit.go's Query builds countQuery and calls countQuery.Count(ctx) before the page select). On redis, Query counts every event that matches before it slices the page, so its Total is the full matching set too.

type ExportReportInput

type ExportReportInput struct {
	ID     string `json:"id"`
	Format string `json:"format"`
}

ExportReportInput names a report and a format: exactly "json", "csv", "markdown" or "html".

type ExportReportResponse

type ExportReportResponse struct {
	Filename    string `json:"filename"`
	ContentType string `json:"contentType"`
	Content     string `json:"content"`
}

ExportReportResponse is a rendered report. Content is the whole document as text, for the client to save as a file named Filename. The html format is a complete page whose values were escaped by html/template; the client should offer it as a download and not inject it into the dashboard's own DOM.

type GenerateCustomReportInput

type GenerateCustomReportInput struct {
	Title    string                `json:"title"`
	Period   *ReportPeriod         `json:"period,omitempty"`
	Sections []CustomReportSection `json:"sections"`
}

GenerateCustomReportInput asks for a report of the caller's own design.

type GenerateReportInput

type GenerateReportInput struct {
	Type   string        `json:"type"`
	Period *ReportPeriod `json:"period,omitempty"`
}

GenerateReportInput asks for one of the fixed frameworks: type is exactly "soc2", "hipaa" or "euaiact". Period is optional; nil, or both bounds empty, means the last 90 days. There is no appId or tenantId field, and none for who generated it: those come from the principal.

type GenerateReportResponse

type GenerateReportResponse struct {
	ID     string        `json:"id"`
	Report ReportSummary `json:"report"`
}

GenerateReportResponse names the report the engine just stored.

type GetCheckpointInput

type GetCheckpointInput struct {
	ID string `json:"id"`
}

GetCheckpointInput names one checkpoint by ID.

type GetCheckpointResponse

type GetCheckpointResponse struct {
	Checkpoint CheckpointSummary `json:"checkpoint"`
}

GetCheckpointResponse is one checkpoint, projected the same way every other checkpoint reference on the wire is (see CheckpointSummary in project.go).

type GetErasureInput

type GetErasureInput struct {
	ID string `json:"id"`
}

GetErasureInput names one erasure record by ID.

type GetEventInput

type GetEventInput struct {
	ID string `json:"id"`
}

GetEventInput names one event by ID.

type GetPolicyInput

type GetPolicyInput struct {
	ID string `json:"id"`
}

GetPolicyInput names one retention policy by ID.

type GetReportInput

type GetReportInput struct {
	ID string `json:"id"`
}

GetReportInput names one report by ID.

type MineInput

type MineInput struct {
	StreamID string `json:"streamId,omitempty"`
}

MineInput optionally names one of the viewer's chains.

StreamID empty means the viewer's own chain: the one for exactly the viewer's app and tenant. An app-wide viewer's own chain is the app-level one (tenant ""), which is empty when every event was recorded under a tenant. Such a viewer finds each tenant's chain through streams.list and passes its ID here. A StreamID the viewer does not own answers NOT_FOUND, the same as one that does not exist.

type MineResponse

type MineResponse struct {
	Stream *StreamSummary `json:"stream,omitempty"`
}

MineResponse carries a nil Stream when the selected scope has no chain, which is a real and common state rather than an error. Chronicle creates a stream lazily on the first Record, and it keeps one per app and tenant, so an app-wide viewer whose events all sit under tenants has no app-level chain even though its tenants have chains of their own. A nil Stream says nothing about those; streams.list does.

type OverviewStats

type OverviewStats struct {
	TotalEvents    int64 `json:"totalEvents"`
	CriticalEvents int64 `json:"criticalEvents"`

	// FailedEvents counts events whose outcome is "failure" and nothing else,
	// which is what the compliance report stats call failedEvents too, so the
	// same key means the same thing on every page. DeniedEvents counts
	// "denied" separately. The templ overview this replaces showed one
	// "failed" tile that was failure plus denied; the React page sums the two
	// fields for that tile, and labels it accordingly.
	FailedEvents int64 `json:"failedEvents"`
	DeniedEvents int64 `json:"deniedEvents"`

	ErasureCount int64 `json:"erasureCount"`

	Categories []AggregateGroupDTO `json:"categories"`
	Severities []AggregateGroupDTO `json:"severities"`
	Outcomes   []AggregateGroupDTO `json:"outcomes"`
}

OverviewStats answers overview.stats: the count tiles and breakdowns on the dashboard's landing page. Every number here is a claim an operator reads at a glance, so each is derived from a scoped store call rather than from a bounded list's length.

type PolicyListResponse

type PolicyListResponse struct {
	Policies []PolicySummary `json:"policies"`
	Total    int             `json:"total"`
}

PolicyListResponse is every retention policy that governs the viewer's scope.

For an app-wide viewer that is every policy in its app, tenants' included, and all of them are editable. For a tenant viewer it is the tenant's own policies (editable) and the app-level policies of its app (not editable): the stores apply an app-level policy to every tenant in the app, so those purge the tenant's history even though the tenant cannot change them. Total counts every row returned.

It is not paged. A policy is unique per (app, tenant, category), so a scope holds one policy per category it governs, and that set is small. Loading all of it is what makes Total a true count rather than the length of a page.

type PolicyPreview

type PolicyPreview struct {
	PolicyID   string `json:"policyId"`
	Category   string `json:"category"`
	EventCount int64  `json:"eventCount"`
	Capped     bool   `json:"capped"`
}

PolicyPreview is what one policy would select if enforcement ran now.

EventCount is capped at previewCap; Capped true means the policy selects at least that many. Two policies in one scope can select the same event (a "*" policy overlaps every category, and an app-level policy overlaps its tenants), so these counts can add up to more than the response's own EventCount, which counts each event once.

type PolicySummary

type PolicySummary struct {
	ID        string `json:"id"`
	Category  string `json:"category"`
	Duration  string `json:"duration"`
	Archive   bool   `json:"archive"`
	AppID     string `json:"appId"`
	TenantID  string `json:"tenantId,omitempty"`
	CreatedAt string `json:"createdAt"`
	UpdatedAt string `json:"updatedAt"`
	Editable  bool   `json:"editable"`
}

PolicySummary is one retention policy as the dashboard reads it.

Duration is Go's duration string ("720h0m0s"), which time.ParseDuration reads back, so the page can round-trip it through retention.savePolicy unchanged.

Editable is true when the viewer may change or delete this policy through retention.savePolicy and retention.deletePolicy, and false for an app-level policy that governs a tenant viewer's chain but belongs to the app. Those are shown by retention.policies and retention.policyDetail so the tenant viewer can see what purges its history, and the two commands answer NOT_FOUND for them. An app-wide viewer owns every policy in its app, so every row it sees is editable.

type ReportDetail

type ReportDetail struct {
	ReportSummary
	Sections          []ReportSection    `json:"sections"`
	Verification      *VerifyReport      `json:"verification,omitempty"`
	VerificationScope *VerificationScope `json:"verificationScope,omitempty"`
}

ReportDetail is a report with its sections and any embedded verification.

Verification is nil when the report carries no integrity verification. A nil value means exactly that and never that verification passed: the page must not render it as a pass. When present it crosses in VerifyReport's full shape, so the page can show the same three states (not checked, checked and held, checked and failed) as the verify page.

VerificationScope says what that verification covered, or why none ran. Read the two together. A valid verification of a capped window proves only the window, so the page shows the range and the notes beside the verdict. A nil VerificationScope comes from a report stored before the engine recorded one, and says nothing about coverage.

There is no raw-data field: compliance.Report.Data is never written.

type ReportListInput

type ReportListInput struct {
	Limit  int `json:"limit,omitempty"`
	Offset int `json:"offset,omitempty"`
}

ReportListInput pages through the viewer's own scope's reports. There is deliberately no appId or tenantId field: scope comes from the principal.

type ReportListResponse

type ReportListResponse struct {
	Reports []ReportSummary `json:"reports"`
	HasMore bool            `json:"hasMore"`
}

ReportListResponse is one page of reports. It carries HasMore and no total: compliance.ReportStore has no count method, so a total would mean counting a bounded list, which understates it on every page after the first. HasMore comes from asking the store for one row past the page.

type ReportPeriod

type ReportPeriod struct {
	From string `json:"from"`
	To   string `json:"to"`
}

ReportPeriod is a report's period on the wire, as RFC 3339 timestamps.

type ReportSection

type ReportSection struct {
	Title           string             `json:"title"`
	Notes           string             `json:"notes,omitempty"`
	Events          []EventSummary     `json:"events"`
	MatchedEvents   int64              `json:"matchedEvents"`
	EventsTruncated bool               `json:"eventsTruncated"`
	Stats           *AggregateResponse `json:"stats,omitempty"`
}

ReportSection is one section of a report with its evidence.

Events is a sample when EventsTruncated is true: the engine caps it at compliance.MaxSectionEvents, and MatchedEvents is how many events matched the section's filters. A page that showed Events without saying so would present a partial listing as complete evidence.

type ReportStats

type ReportStats struct {
	TotalEvents    int64 `json:"totalEvents"`
	CriticalEvents int64 `json:"criticalEvents"`
	FailedEvents   int64 `json:"failedEvents"`
	DeniedEvents   int64 `json:"deniedEvents"`
}

ReportStats is compliance.Stats on the wire. The counts are exact for the whole period and scope: the engine aggregates them separately from the sections, whose embedded events are capped.

type ReportSummary

type ReportSummary struct {
	ID          string       `json:"id"`
	Title       string       `json:"title"`
	Type        string       `json:"type"`
	Period      ReportPeriod `json:"period"`
	GeneratedBy string       `json:"generatedBy"`
	Format      string       `json:"format"`
	CreatedAt   string       `json:"createdAt"`
	Stats       *ReportStats `json:"stats,omitempty"`
	// TenantID is empty for an app-level report. Sent even when empty; see
	// EventSummary.TenantID.
	TenantID string `json:"tenantId"`
}

ReportSummary is one report as a list shows it. It carries no sections and so no events: a report can embed up to MaxSectionEvents events per section, and a list of those would dominate the response. reports.detail carries them.

Type is the engine's own stored value, which is not always the value reports.generate accepted: a report requested as "euaiact" is stored, and reported here, as "eu_ai_act".

type RequestErasureInput

type RequestErasureInput struct {
	SubjectID string `json:"subjectId"`
	Reason    string `json:"reason"`
}

RequestErasureInput is what an operator supplies to erase a data subject. There is no requestedBy field, and no appId or tenantId: who asked comes from the session and where it runs comes from scopeFromPrincipal, so neither can be named by the request.

type RetainedRangeDTO

type RetainedRangeDTO struct {
	// FromSeq and ToSeq bound the run of removed sequences, both inclusive.
	FromSeq uint64 `json:"fromSeq"`
	ToSeq   uint64 `json:"toSeq"`

	// RecordSeq is the sequence of the retention record that lists the run.
	// It is an ordinary event in the chain, so the page can link to it.
	RecordSeq uint64 `json:"recordSeq"`

	// PolicyID is the retention policy the record says it acted under. It is
	// the record's own claim, and it names a policy that may since have been
	// deleted. Empty when the record names none.
	PolicyID string `json:"policyId,omitempty"`

	// Backfill names the archive the record was recovered from, when it was
	// written afterwards for a purge that predates retention records. Empty
	// for a record the enforcer wrote at purge time, which is the ordinary
	// case.
	Backfill string `json:"backfill,omitempty"`
}

RetainedRangeDTO is verify.RetainedRange on the wire: a run of sequences a retention policy removed, which an authentic retention record in the chain accounts for. The page shows it as an authorised, recorded purge, not as a deletion.

A retained range asserts linkage, never content. The record vouches that these sequences existed and gives the hashes either side of them, so the chain still links across the hole. It says nothing about what the removed events contained, and the events themselves are gone from the store.

type RetentionPreviewResponse

type RetentionPreviewResponse struct {
	EventCount           int64           `json:"eventCount"`
	Capped               bool            `json:"capped"`
	NoPolicies           bool            `json:"noPolicies"`
	GoverningAppPolicies int             `json:"governingAppPolicies"`
	ByPolicy             []PolicyPreview `json:"byPolicy"`
}

RetentionPreviewResponse describes the events the viewer's policies make eligible for purging right now, for the confirm dialog that stands in front of retention.enforce.

The counts are eligible events, not what one enforce pass will delete. The preview counts up to previewCap (10,000) events per policy, and one pass purges at most retention.DefaultPurgeBatchSize (5,000) per policy, so a large backlog takes several passes. EnforceResponse.MoreRemain is what says another one is needed.

EventCount is the number of distinct events the viewer's own policies select. Capped true means at least one policy hit previewCap, so the real number is at least EventCount.

NoPolicies true means the viewer has no policy of its own, which is a different answer from zero-because-nothing-is-old, and the page has to be able to say which one it is. It does not mean nothing can purge this scope: GoverningAppPolicies counts the app-level policies that also purge a tenant viewer's chain. retention.enforce runs only the viewer's own policies, so those are neither previewed nor enforced from here; the background scheduler enforces them. It is always zero for an app-wide viewer, whose app-level policies are its own and appear in ByPolicy.

type SavePolicyInput

type SavePolicyInput struct {
	ID       *string `json:"id,omitempty"`
	Category *string `json:"category,omitempty"`
	Duration *string `json:"duration,omitempty"`
	Archive  *bool   `json:"archive,omitempty"`
}

SavePolicyInput creates or updates a retention policy.

Every field is a pointer so an update can leave one alone. A plain bool Archive would be false on every update that did not mention it, and would quietly stop a policy archiving before it purges.

ID absent means create: Category and Duration are then required, and the policy is stamped with the viewer's own app and tenant. ID present means update: the handler fetches the policy, checks the viewer owns it, and applies only the fields that were supplied. There is deliberately no appId or tenantId field. A policy whose scope came from the request could be written with an empty AppID, which the stores read as "every app", and the next enforcement run would purge every tenant's history.

type SettingsDetail

type SettingsDetail struct {
	BatchSize           int    `json:"batchSize"`
	FlushInterval       string `json:"flushInterval"`
	RetentionInterval   string `json:"retentionInterval"`
	EnableCryptoErasure bool   `json:"enableCryptoErasure"`

	// DigestScheme is what this process writes under now. Existing chains may
	// be pinned to a different scheme below their pin: a chain that had HMAC
	// turned on later is unkeyed below it forever. streams.mine reports that
	// per chain.
	DigestScheme string `json:"digestScheme"`

	// Keyed is whether DigestScheme depends on key material. It is derived
	// from the scheme and not from the presence of a key provider, so it
	// cannot disagree with DigestScheme.
	Keyed bool `json:"keyed"`

	// CheckpointingConfigured is true only when a checkpoint store AND a
	// signer are both present.
	CheckpointingConfigured bool `json:"checkpointingConfigured"`

	BackendName string `json:"backendName"`

	// BackendHoldsCheckpoints is false only when the backend has said it
	// cannot store checkpoints at all (redis does this).
	BackendHoldsCheckpoints bool `json:"backendHoldsCheckpoints"`
}

SettingsDetail is what this deployment is configured to do, which for Chronicle is mostly a statement about how much its verification is worth.

The digest, checkpoint and backend fields are the reason this intent exists. The templ settings page showed batch sizes and intervals, which nobody needs, and said nothing about the digest scheme, checkpointing or the backend, which is what tells an operator what a "verified" result means here.

type StreamListInput

type StreamListInput struct {
	Limit  int `json:"limit"`
	Offset int `json:"offset"`
}

StreamListInput pages through the chains the viewer owns.

type StreamListResponse

type StreamListResponse struct {
	Streams []StreamSummary `json:"streams"`
	Total   int64           `json:"total"`
	HasMore bool            `json:"hasMore"`
}

StreamListResponse is one page of the viewer's chains. Total counts every chain the viewer owns, not every chain in the store.

type StreamSummary

type StreamSummary struct {
	ID       string `json:"id"`
	AppID    string `json:"appId"`
	TenantID string `json:"tenantId,omitempty"`
	HeadHash string `json:"headHash"`
	HeadSeq  uint64 `json:"headSeq"`

	Scheme      string `json:"scheme"`
	SchemeSince uint64 `json:"schemeSince"`

	// CoverageCeiling is the best assurance level this deployment could
	// report for this chain, before any verification runs. It is what lets
	// the page tell an operator what is switched off without walking the
	// chain first, which for a default deployment is the most useful thing
	// it can say.
	CoverageCeiling string `json:"coverageCeiling"`

	// LatestCheckpoint is nil when this deployment takes no checkpoints, or
	// when this chain has none yet. The two are different and the page says
	// which: CheckpointingConfigured distinguishes them.
	LatestCheckpoint        *CheckpointSummary `json:"latestCheckpoint,omitempty"`
	CheckpointingConfigured bool               `json:"checkpointingConfigured"`
}

StreamSummary is one chain as the dashboard reads it.

type SurfaceConfig

type SurfaceConfig struct {
	BatchSize           int
	FlushInterval       string
	RetentionInterval   string
	EnableCryptoErasure bool
	BackendName         string
}

SurfaceConfig is what settings.detail reports about this deployment.

type TakeCheckpointInput

type TakeCheckpointInput struct {
	StreamID string `json:"streamId,omitempty"`
}

TakeCheckpointInput optionally names the chain to checkpoint. Empty means the viewer's own chain; see CheckpointListInput for what a StreamID is.

type TakeCheckpointResponse

type TakeCheckpointResponse struct {
	Checkpoint *CheckpointSummary `json:"checkpoint,omitempty"`
	UpToDate   bool               `json:"upToDate"`
}

TakeCheckpointResponse is the result of checkpoints.take.

Taking a checkpoint over a stream that gained nothing since its last one, or losing a race to a concurrent checkpointer that already landed at the same sequence, are both ordinary outcomes on a healthy deployment, not failures -- see checkpoint.ErrNothingToCheckpoint and checkpoint.ErrExists. Both come back here as UpToDate true with a nil Checkpoint and no error, rather than as CodeInternal, which would alarm an operator over a normal no-op.

type VerificationScope

type VerificationScope struct {
	Status                string   `json:"status"`
	StreamID              string   `json:"streamId,omitempty"`
	Scheme                string   `json:"scheme,omitempty"`
	SchemeSince           uint64   `json:"schemeSince,omitempty"`
	HeadSeq               uint64   `json:"headSeq"`
	FromSeq               uint64   `json:"fromSeq"`
	ToSeq                 uint64   `json:"toSeq"`
	Window                uint64   `json:"window"`
	Capped                bool     `json:"capped"`
	CheckpointsConfigured bool     `json:"checkpointsConfigured"`
	Notes                 []string `json:"notes"`
}

VerificationScope is what a report's chain verification covered.

Status is "verified" when it ran, and then Verification holds the result. "no_chain" means the report's scope never recorded an event, and "not_configured" means the deployment gave the report engine no hash chain, so nothing was checked. Neither of those is a pass.

FromSeq and ToSeq are the sequences verification was asked to cover, ending at HeadSeq. Capped is true when the chain was longer than Window, and then every sequence before FromSeq went unchecked. Notes are the engine's own caveats, and every export renders them, so the page shows them too.

A report generated with no tenant verified only the app's untenanted stream, never any tenant's chain, and the engine says so in Notes. So "verified, not capped" on an app-wide report is not a pass for the tenants.

type VerifyEventInput

type VerifyEventInput struct {
	EventID string `json:"eventId"`
}

VerifyEventInput names one event by ID. There is no scope on it: the handler resolves scope from the principal and refuses an event that does not belong to it, the same as every other detail intent.

type VerifyEventResponse

type VerifyEventResponse struct {
	Valid      bool   `json:"valid"`
	HashScheme string `json:"hashScheme"`
	Keyed      bool   `json:"keyed"`
}

VerifyEventResponse is everything Chronicle.VerifyEvent(eventID) can actually supply. It is a bare (bool, error): the OK/Downgrade/Tolerant distinction that hash.Result carries is flattened before it gets here, and the hasher that could recompute it is unexported.

HashScheme is the event's own recorded scheme, verbatim, which can be empty, and what an empty one means depends on where the event sits. Below the stream's pin (or on a chain with no pin) it means the row predates schemes being recorded: its digest was resolved by the tolerant, pre-migration fallback (trying the historical unkeyed schemes in turn) rather than checked against a claimed one, and Keyed is false for it. At or above the pin an empty scheme is not history, since clearing the column is how a downgrade would buy the tolerant path, so it comes back Valid false.

Two further limits bound what a caller may conclude from Valid, and neither can be recovered from the response:

  • On an unkeyed chain, Valid true does not rule out a rewrite. The digest is reproducible by anyone who can write to the store, so a rewritten event that also recomputed its own hash and PrevHash passes this check the same as an untouched one.
  • This checks only the event's own digest against its claimed PrevHash, never whether that PrevHash matches the event actually stored before it. It says nothing about the event's place in the chain. Chain linkage needs verify.run over a range around the event.

A downgrade (an event claiming a weaker scheme than its stream pins at that sequence, which verify.Report surfaces as its own Downgrades list) has no representation here at all: it can only come back as Valid false, indistinguishable from ordinary edited content. Telling the two apart needs verify.run over a range around the event, not this intent.

type VerifyInput

type VerifyInput struct {
	StreamID string `json:"streamId,omitempty"`
	FromSeq  uint64 `json:"fromSeq,omitempty"`
	ToSeq    uint64 `json:"toSeq,omitempty"`
}

VerifyInput bounds the range. Both zero means genesis to head, which is the whole chain. The React page sends an explicit bounded window by default, because VerifyChain holds every event in the range in memory at once; the server enforces that bound too, through Deps.MaxVerifySpan, because a client that forgot to bound its request must not be able to crash the server.

StreamID optionally names the chain to verify. Empty means the viewer's own chain (its exact app and tenant). An app-wide viewer passes a tenant's chain ID, taken from streams.list, to verify that tenant's history. A chain the viewer does not own answers NOT_FOUND.

type VerifyReport

type VerifyReport struct {
	Valid      bool     `json:"valid"`
	Verified   int64    `json:"verified"`
	Gaps       []uint64 `json:"gaps,omitempty"`
	Tampered   []uint64 `json:"tampered,omitempty"`
	Downgrades []uint64 `json:"downgrades,omitempty"`
	Tolerant   []uint64 `json:"tolerant,omitempty"`
	FirstEvent uint64   `json:"firstEvent"`
	LastEvent  uint64   `json:"lastEvent"`
	HeadSeq    uint64   `json:"headSeq"`

	Partial     bool `json:"partial"`
	HeadMatch   bool `json:"headMatch"`
	HeadChecked bool `json:"headChecked"`

	CheckpointsChecked    bool `json:"checkpointsChecked"`
	CheckpointHeadOK      bool `json:"checkpointHeadOk"`
	CheckpointHeadChecked bool `json:"checkpointHeadChecked"`

	// Retained lists the missing sequences that a retention policy removed
	// and an authentic retention record accounts for, as ranges. They are not
	// in Gaps and do not make the chain invalid. Absent when there are none.
	Retained []RetainedRangeDTO `json:"retained,omitempty"`

	Coverage    []CoverageSpan     `json:"coverage,omitempty"`
	Checkpoints []CheckpointResult `json:"checkpoints,omitempty"`

	// RetentionPolicies is how many retention policies can purge the chain
	// this report verified. Purges those policies recorded in the chain are
	// reported in Retained and are not gaps. A non-zero value therefore means
	// only this: Gaps may still include purges that were never recorded in
	// the chain, which Chronicle cannot tell from deletion. That happens for
	// purges from before retention records existed that were never
	// backfilled, for an enforcer built without a chain recorder, and for a
	// record that does not verify under the chain's pin. Such a purged
	// sequence reads as a gap. The event after it is checked against its own
	// declared predecessor, so an unrecorded purge alone does not mark it
	// tampered.
	//
	// A chain is one (app, tenant). A policy purges it when the policy's
	// app is the chain's app and the policy's tenant is either empty (the
	// stores apply an app-level policy to every tenant in the app) or the
	// chain's own tenant. A policy with no app at all, which only trusted
	// in-process code can create, is not counted.
	//
	// It counts the policies configured NOW, so purges made under policies
	// that have since been deleted are not reflected. -1 means unknown:
	// listing the policies failed, and that is not allowed to fail the
	// verification itself. It is also -1 on a verification embedded in a
	// stored report (reports.detail), where no count was taken. No omitempty,
	// because zero is an answer the page needs.
	RetentionPolicies int `json:"retentionPolicies"`
}

VerifyReport is verify.Report on the wire. Nothing is summarised and no flag is dropped: the page renders three states from each checked pair ("not checked", "checked and held", "checked and failed") and cannot reconstruct one that did not cross. That is why none of the six checked flags or their value partners carry omitempty: omitempty on a false bool would erase exactly the distinction this whole design exists to show.

type VerifyResponse

type VerifyResponse struct {
	Report  *VerifyReport `json:"report,omitempty"`
	NoChain bool          `json:"noChain"`
}

VerifyResponse carries NoChain rather than only a nil Report, because "the selected scope has never recorded an event" and "verification produced nothing" are different answers and the page says which.

NoChain is about one scope. Chronicle keeps a chain per app and tenant, so an app-wide viewer whose events all sit under tenants gets NoChain for its own app-level scope while each tenant's chain, listed by streams.list, has events and can be verified by naming it.

Jump to

Keyboard shortcuts

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