Documentation
¶
Overview ¶
Package shared holds what every use case needs regardless of its context: who is acting, in which tenant, with which permissions, and in which language the answer is to be rendered.
The actor is a typed wrapper, not a bare context value. Business code asks the wrapper, never the context (project-structure.md §3) - which is what makes "who may do this" a question with exactly one answer and one place to look for it.
Index ¶
- Constants
- func ContainerNotFound(id shared.ID) error
- func ContextWithActor(ctx context.Context, actor ActorContext) context.Context
- func ContextWithDevice(ctx context.Context, deviceID shared.ID) context.Context
- func ContextWithPush(ctx context.Context, push Push) context.Context
- func ContextWithReadings(ctx context.Context, readings map[string]shared.HLC) context.Context
- func DeviceFrom(ctx context.Context) shared.ID
- func ItemNotFound(id shared.ID) error
- func ReadingFrom(ctx context.Context, field string) (shared.HLC, bool)
- type ActorContext
- func (a ActorContext) ForACopy() ActorContext
- func (a ActorContext) HasScope(scope string) bool
- func (a ActorContext) IsAuthenticated() bool
- func (a ActorContext) PersistenceScope() persistence.Scope
- func (a ActorContext) RequireAnyScope(scopes ...string) error
- func (a ActorContext) RequireScope(scope string) error
- type ActorKind
- type Push
- type WithheldScopes
Constants ¶
const ( ActorAnonymous = shared.ActorAnonymous ActorUser = shared.ActorUser ActorServiceAccount = shared.ActorServiceAccount ActorAutomation = shared.ActorAutomation ActorAIAgent = shared.ActorAIAgent ActorSystem = shared.ActorSystem )
Variables ¶
This section is empty.
Functions ¶
func ContainerNotFound ¶
ContainerNotFound is the answer for a hub or collection the actor cannot reach, whatever the reason: the one a use case gives for an identifier that is not in the tenant, so that a private hub and a missing one read alike (T-04, ADR-0073 §1).
func ContextWithActor ¶
func ContextWithActor(ctx context.Context, actor ActorContext) context.Context
ContextWithActor carries the actor into the call chain. Only the inbound adapters call this: they are where a credential is turned into an actor, and a use case that could construct its own actor could grant itself a tenant.
func ContextWithDevice ¶
ContextWithDevice marks everything done under the context as one device's act, so that the change log entries a push produces name the device - which is what lets that device skip its own echo (offline-sync.md §10, `change_log.device_id`).
A context value rather than a field on every writer: fifty-five use cases record changes, and what a push adds to each of them is exactly one fact about the caller, which is what a context is for. The change log adapter reads it when a change names no device of its own.
func ContextWithPush ¶
ContextWithPush marks everything done under the context as one mutation of one push. The outbox adapter reads it when an event is appended, the change log adapter's way: fifty-five writers raise events, and what a push adds to each is one fact about the caller.
func ContextWithReadings ¶
ContextWithReadings marks the context with the readings a device wrote its fields under, by field name. A use case a push performs records its change log entry as it always does, and the change log adapter takes the device's reading for a field named here in place of the writer's fresh one - so that the clock a second device later compares against is the clock that decided the merge (offline-sync.md §4.2). Fields not named keep the writer's reading.
func DeviceFrom ¶
DeviceFrom answers the device the context was marked with, or the empty identifier for a change made through the API rather than through a push.
func ItemNotFound ¶
ItemNotFound is the one answer for an entry the actor cannot reach, whatever the reason.
It lives here rather than in the package that reads entries because two layers have to give exactly the same answer, down to the message code and the parameters. A repository says it when the row is not in the tenant; the authorisation service says it when nothing on the entry's path grants the actor anything (T-04). If the two differed by a single character, the difference would be an oracle for which identifiers exist - which is precisely the disclosure the not-found answer is there to prevent (multi-tenancy.md §2).
Types ¶
type ActorContext ¶
type ActorContext struct {
Kind ActorKind
// TenantID is empty exactly when the actor is anonymous.
TenantID shared.ID
// TenantSlug is the workspace's public name, riding with the credential so the
// middleware can compare §3's weaker resolution sources - the subdomain and the header -
// against what the credential says without a second lookup.
TenantSlug string
// RateLimitPerMinute is the workspace's own request-rate ceiling, riding with the
// credential for the limiter's sake. 0 means none is configured - the installation's
// default applies, at the level that already enforces it.
RateLimitPerMinute int64
// AccountID is the acting account. Empty for the system itself.
AccountID shared.ID
// AccountName is the label the audit trail records next to the identifier. It travels with
// the actor because the trail denormalises it: an entry that only points at a foreign key
// becomes unreadable once the account is deleted (audit.md §2, test AT-7). It is personal
// data and goes nowhere else - not into a log, a metric, a span, or an event.
AccountName string
// TokenID identifies the credential used, for the audit trail and for revocation. Empty on an
// interactive session.
TokenID shared.ID
// Scopes are the token's bounds. They are a second, independent limit on top of the role: a
// token can never do more than its scopes allow, whatever role its owner holds (ADR-0005).
Scopes []string
// Withheld are the scopes the credential names that the operator register took away
// (ADR-0070 §1). They are not held - `HasScope` never sees them - and they exist so that a
// refusal can say the register is the reason (UC-INS-03 check 5).
Withheld WithheldScopes
// RunsForRule is the automation rule this act runs for, set by the rule engine and nowhere else;
// empty for every act a caller makes itself. The engine acts as the rule's service account, which
// a caller holding that account's token would be too - only this tells an act whose answer
// nobody reads from one whose caller reads it.
RunsForRule shared.ID
// Present is true for a request a person is waiting on now: a call through the REST API - the
// web app's door and hubctl's - set by that adapter and nowhere else. It decides one thing:
// whether the workspace owner's running emergency access to a private hub counts (ADR-0073 §4,
// identity.md §22). The access is for the person looking, for one hour, and must leave nothing
// behind it: so it never counts for a device's copy (the sync service takes the mark off,
// ForACopy), a calendar subscription, a job or a rule run later (built without it), or an AI
// agent acting with the person's credential. False is the safe value: an actor somebody forgot
// to mark sees nothing of a private hub.
Present bool
// APIClient is the OAuth client the credential was issued to, empty for everything
// that is not a grant session. It is what the audit trail records as the acting app.
APIClient shared.ID
// Locale is BCP 47, resolved request → account → tenant → installation (i18n-l10n.md §2).
Locale string
// TimeZone is an IANA name. Every relative date in a query and every reminder is computed in
// it, so it travels with the actor rather than being looked up per use case.
TimeZone string
// WeekStart is `MONDAY`, `SUNDAY` or `SATURDAY`: the account's own preference where it set
// one, otherwise the day the week starts where its locale is spoken (i18n-l10n.md §4).
// The week anchors of a query - `@start_of_week`, `@end_of_week` - are computed from it, so
// it travels here beside the zone rather than being looked up per use case. Empty is Monday.
WeekStart string
}
ActorContext is the request as the application layer sees it.
TenantID comes from authentication and from nowhere else - never from a path, a body, or a header a client controls (multi-tenancy.md §2.2). It is the value the transaction wrapper turns into `SET LOCAL app.tenant_id`, so anything that could set it from the request would be a way around row level security.
func ActorFrom ¶
func ActorFrom(ctx context.Context) (ActorContext, bool)
ActorFrom returns the actor of the request.
The zero value is deliberately not an anonymous actor but an unusable one: a caller that forgot the middleware gets a context whose IsAuthenticated is false and whose tenant is empty, so the unit of work refuses it. Failing to find an actor must not look like finding a valid one.
func Anonymous ¶
func Anonymous(locale, timeZone string) ActorContext
Anonymous is the actor of a request that carried no credential. It has a locale, because an error still has to be rendered in some language, and nothing else.
func (ActorContext) ForACopy ¶
func (a ActorContext) ForACopy() ActorContext
ForACopy is the actor as a reader that fills a copy which outlives the request - a device's replica - for which a running emergency access does not count (Present).
func (ActorContext) HasScope ¶
func (a ActorContext) HasScope(scope string) bool
HasScope reports whether the credential carries the scope.
func (ActorContext) IsAuthenticated ¶
func (a ActorContext) IsAuthenticated() bool
IsAuthenticated reports whether a credential was accepted. A tenant without an actor kind, or an actor kind without a tenant, is not authentication - it is a half-built context, and treating it as authenticated is how a boundary gets crossed.
func (ActorContext) PersistenceScope ¶
func (a ActorContext) PersistenceScope() persistence.Scope
PersistenceScope is what bounds a unit of work. It exists here so that the translation from "who is acting" to "which tenant the transaction runs as" happens once, rather than in every use case that opens one.
func (ActorContext) RequireAnyScope ¶
func (a ActorContext) RequireAnyScope(scopes ...string) error
RequireAnyScope admits a credential carrying any of the scopes, the first being the one an operation is declared under and the rest narrower ones that admit it as well (usecase.Descriptor.AlsoScope). A refusal names the last - the narrowest that would do - unless the register withheld one of them, which is then the reason given.
func (ActorContext) RequireScope ¶
func (a ActorContext) RequireScope(scope string) error
RequireScope is the second of the two bounds on an operation; the first is the role, and both have to allow it (ADR-0005). The refusal names the scope the client is missing, because a client that cannot tell which scope to ask for cannot fix the problem (api-guidelines.md §7).
type ActorKind ¶
ActorKind is the actor type the audit trail records (audit.md §2). Anonymous is the one value that never reaches an audit entry: an unauthenticated request performs no auditable action.
An alias rather than a type of its own: an event, an audit entry and a request context have to spell the five kinds identically, so there is one definition of them, in the domain (core/domain/model/shared/Actor.go). The names are re-exported here because this is where application code reaches for them.
type Push ¶
Push is one push as the events it raises need to know it (offline-sync.md §8): which push, so that the fan-out collapses the deliveries of one push; and the device's bounded reading of the moment the change was made, which becomes the event's occurred_at while the writer's own clock becomes when it was received.
type WithheldScopes ¶
type WithheldScopes struct {
Scopes []string
// Unreadable is true when the register could not be read, so the scope was withheld closed
// rather than because the holder is outside it.
Unreadable bool
}
WithheldScopes is what the operator register removed from a credential, and why.