Documentation
¶
Overview ¶
Package dataentry provides a config-driven data entry web application built on top of rela's metamodel system. It reads a data-entry.yaml config file alongside a rela project and serves an interactive UI for CRUD operations on entities stored as markdown files.
Configuration types and validation logic live in the dataentryconfig package so that the CLI can validate configs without importing the full web layer. This file re-exports those types for backward compatibility.
Index ¶
- Constants
- Variables
- func CheckEmbeddedSPA() error
- func RewriteDocumentLinks(htmlContent, returnPath string, log *slog.Logger) string
- func SetWorldNeighbors(a *App, s store.Store, classes worldreader.ScopeClassifier) error
- type APIAnalysisResult
- type APIDefaultOverride
- type APIIssue
- type APIPropertyDef
- type APIRelationDef
- type APIRelationTarget
- type APISettingsData
- type APIThemeImportResponse
- type APIUserDefaults
- type AffordanceDenialError
- type AffordanceDenialRule
- type AffordanceProfile
- type AnalysisIssue
- type AnalysisResult
- type AnalysisSection
- type App
- func (a *App) Cfg() *Config
- func (a *App) Meta() *metamodel.Metamodel
- func (a *App) NewRouter() http.Handler
- func (a *App) ProjectName() string
- func (a *App) ProjectRoot() string
- func (a *App) Services() Services
- func (a *App) SetCalDAVAliases(s *caldavalias.Service)
- func (a *App) SetJWTGate(cfg JWTGateConfig) error
- func (a *App) SetNextActionMatchers(fn NextActionMatcherFunc) error
- func (a *App) SetPrincipalHeader(name string)
- func (a *App) SetPrincipalResolver(r PrincipalResolver)
- func (a *App) SetRemoteMCP(factory MCPHandlerFactory) error
- func (a *App) SetSecurityConfig(cfg SecurityConfig) error
- func (a *App) SetUserState(s userstate.Store) error
- func (a *App) SetWebhookReceiver(v webhookVerifier, actionID string)
- func (a *App) SetWorlds(w WorldLookup)
- func (a *App) StartGitFetch() (stop func())
- func (a *App) StartWatching() error
- func (a *App) State() *Schema
- func (a *App) StopWatching()
- type AppConfig
- type AssertedIdentity
- type Calendar
- type CalendarSource
- type CommandConfig
- type CommandMessage
- type CommandScope
- type Config
- type ConfigValidationError
- type DashboardCard
- type DashboardConfig
- type DefaultOverride
- type DemoFieldVerdictResolver
- type Direction
- type DocumentConfig
- type DocumentResult
- type EnumHelp
- type FieldVerdictResolver
- type FieldVerdicts
- type FilterConfig
- type FilterControl
- type Form
- type FormField
- type FormRelation
- type Gantt
- type GitStatusResponse
- type GitSyncResponse
- type GroupData
- type ImportedThemeAsset
- type JWTGateConfig
- type Kanban
- type KanbanCard
- type KanbanColumn
- type KanbanSwimlane
- type List
- type ListColumn
- type MCPHandlerFactory
- type NavigationEntry
- type NextActionMatcherFunc
- type NopFieldVerdictResolver
- type PaletteColors
- type PaletteConfig
- type ParsedThemePackage
- type PrincipalResolver
- type PropertyHelp
- type RelationHelp
- type RelationOp
- type RelationProperty
- type RelationVerdict
- type RelationVerdicts
- type RelationVisibilityResolver
- type ResolvedCommand
- type ResolvedPalette
- type ResolverServices
- type Schema
- type ScopeDescriptor
- type ScriptErrorEnvelope
- type ScriptErrorLua
- type ScriptIdentity
- type SectionAddInfo
- type SectionAddTarget
- type SectionColumnData
- type SectionData
- type SectionEntityData
- type SectionFieldData
- type SectionLinkInfo
- type SectionRowData
- type SecurityConfig
- type Services
- type SidePanelConfig
- type SortSpec
- type TransitionHelp
- type TransitionResolver
- type UserDefaults
- type ValueHelp
- type ViewConfig
- type ViewEntry
- type ViewSection
- type ViewSectionField
- type ViewTraverse
- type Warning
- type WebhookClaims
- type WorldLookup
Constants ¶
const ( WidgetText = dataentryconfig.WidgetText WidgetSelect = dataentryconfig.WidgetSelect WidgetMultiSelect = dataentryconfig.WidgetMultiSelect WidgetCheckbox = dataentryconfig.WidgetCheckbox WidgetTextarea = dataentryconfig.WidgetTextarea WidgetNumber = dataentryconfig.WidgetNumber WidgetDate = dataentryconfig.WidgetDate WidgetDatetime = dataentryconfig.WidgetDatetime WidgetCards = dataentryconfig.WidgetCards DirectionIncoming = dataentryconfig.DirectionIncoming DirectionOutgoing = dataentryconfig.DirectionOutgoing )
Widget and direction constants — re-exported from dataentryconfig.
const ConfigFile = dataentryconfig.ConfigFile
ConfigFile is the conventional filename for data-entry configuration within a rela project.
const DefaultMaxAttachmentBytes = 64 << 20 // 64 MiB
DefaultMaxAttachmentBytes is the product-wide default cap on a single attachment, applied at the HTTP upload ingress. It is generously sized for the expected use (screenshots, PDFs, office docs), not media. A deployment can override it via dataentryconfig; the store backends also enforce their own backstop guard so no path is ever unbounded.
const DefaultWebhookMaxBodyBytes int64 = 1 << 20
DefaultWebhookMaxBodyBytes caps an inbound declarative-webhook body. The parsed body becomes an in-memory map that every template interpolation reads, so this bounds real memory per concurrent delivery — the action endpoint has no such cap, which is precisely the gap this closes.
1 MiB is generous for the payload shapes this serves (a monitoring alert, a form post, an upstream event) while staying small enough that a burst of concurrent deliveries cannot exhaust the heap.
const EnvDataEntryUserVar = "RELA_DATAENTRY_USER"
EnvDataEntryUserVar is the local-dev escape hatch: if this env var is set, EnvPrincipalResolver returns its value as the principal user. Documented in docs/server-security.md alongside the --principal-header flag.
Exported so cmd/rela-server can reject it alongside --jwt-* without duplicating the literal: under verified-JWT identity an env var that overrides a cryptographically proven subject is the same downgrade the header fall-through was.
const MCPPath = "/api/v1/_mcp"
MCPPath is the mount point for the remote MCP endpoint.
It lives under `/api/` on purpose: [isAPIPath] matches it, so the endpoint inherits the full request chain — `stampAuditPrincipal` → `requireVerifiedJWT` → `attachACLRequest` — with no middleware change. A mount outside `/api/` would silently bypass both the identity gate and the ACL, which is exactly the failure RR-P2M7 guards against for the bare `/api`.
const ( // MaxUserLogoBytes caps user-uploaded logos. Sidebar logos render at // ~28px tall; 256 KiB is generous for that display size. MaxUserLogoBytes = 256 << 10 )
User-uploaded theme assets are stored under .rela/theme/. The bytes file ("theme/logo") is opaque; the sidecar ("theme/logo.ext") records the inferred extension so the GET handler can set Content-Type without re-sniffing on every request.
const ThemePackageMaxBytes = 5 << 20
ThemePackageMaxBytes caps the on-the-wire size of a `.relatheme` upload. The manifest is at most a couple of KB; a logo is ≤256 KiB. 5 MiB leaves generous headroom while bounding the worst-case memory pressure a single import can impose.
const WorldParam = "world"
WorldParam is the query parameter that selects a world on the read API: `?world=published`. Absent or empty means the DEFAULT world, bound explicitly at this boundary — the interior never sees "unspecified" (design doc §4.4).
Variables ¶
var CollectConfigWarnings = dataentryconfig.CollectConfigWarnings
CollectConfigWarnings re-exports the non-fatal config-warning collector.
var ResolvePalette = dataentryconfig.ResolvePalette
ResolvePalette is re-exported from dataentryconfig.
var ValidateConfig = dataentryconfig.ValidateConfig
ValidateConfig re-exports the validation function from dataentryconfig.
Functions ¶
func CheckEmbeddedSPA ¶
func CheckEmbeddedSPA() error
CheckEmbeddedSPA verifies that the embedded Vue SPA bundle is present and usable. Production entry points (cmd/rela-server, cmd/rela-desktop) should call this at startup so a missing or empty build fails loudly with a clear message instead of silently serving a directory listing (the BUG-W144 regression class). Tests that construct routers via NewRouter do not need to call this.
func RewriteDocumentLinks ¶
RewriteDocumentLinks walks all href="..." attributes in rendered HTML and rewrites internal links so the SPA can offer a back affordance.
The rewriter runs AFTER the document-render cache (see documentService.GetCached / Render in this package, and the call sites in api_v1.go). It never writes to the cache. This is load-bearing: the cache file is keyed on the entry entity's content hash and must NOT contain any `return_to=` tokens, so that two viewers requesting the same entry under different return_to values each get their own value rewritten in. Do not move this step into doRender.
Behavior, by path class × returnPath presence:
| Path class | returnPath == "" | returnPath != "" | |----------------------------|-------------------------------|---------------------------------------| | Form (/form/<id>[/...]) | strip return_to; emit id | strip return_to; emit id; inject ours | | Non-form internal (/...) | strip return_to; pass through | strip return_to; inject ours | | External / mailto / anchor | passthrough unchanged | passthrough unchanged | | Legacy edit:// / create:// | log warning; passthrough | log warning; passthrough |
Author-supplied `return_to` values on internal links are always stripped, whether or not we have a replacement: the rewriter is the single source of truth for the key on emitted HTML.
Form routes additionally get a stable id="edit-<entityID>-<n>" or id="create-<form>-<n>" attribute so the SPA's document click handler can record a scroll-back anchor that survives title/content edits. The per-base counter (<n>) disambiguates multiple links to the same target within a single rendered document and is stable across re-renders that produce the same link sequence.
The rewriter is idempotent: applying it twice with the same returnPath produces the same bytes as one pass. Applying it twice with different returnPaths yields the last one injected (the first is stripped, then the second is injected).
func SetWorldNeighbors ¶
func SetWorldNeighbors(a *App, s store.Store, classes worldreader.ScopeClassifier) error
SetWorldNeighbors enables world-scoped LINK resolution (`?world=` on a response's relations and `?include=`).
A package-level FUNCTION rather than a method on App, for the reason the world code has taken this shape throughout: App carries a `//plimsoll:max-methods=104` directive pinning it at its current count, and the project rule is to split the type rather than raise the number. The world feature has added ONE method to App so far (App.SetWorlds) and four package functions (resolveWorld, attachWorld, worldCapablePath, and this), which is the discipline recorded on the App type doc.
Not calling this is a valid state: relations then behave as they did before TKT-WRLDAPI item 4 — present under the default world, absent under any other. That is safe but incomplete, which is why the composition root wires it whenever it wires App.SetWorlds.
classes classifies a relation type as content- or identity-scoped; it is supplied by the wiring site because the dispatch it feeds (worldreader.RelationReader) must not be reimplemented here.
Nil: rejected — a nil app, store or classifier returns an error rather than silently leaving link resolution off, which would present as "this world has no links" on every page.
Types ¶
type APIAnalysisResult ¶
type APIAnalysisResult struct {
Errors int `json:"errors"`
Warnings int `json:"warnings"`
Issues []APIIssue `json:"issues"`
ByCheck map[string]int `json:"byCheck"`
// TruncatedChecks names the checks that found more issues than they
// returned, so the UI can mark those lists as incomplete (TKT-1ESTYJ).
//
// Per-CHECK rather than one global flag: "duplicates is truncated" is
// actionable where "something was truncated" is not, and the response
// is a flat issue list, so the section-level flag would otherwise be
// lost. Empty (omitted) when every check reported in full.
//
// Counts in ByCheck are counts of RETURNED issues; for a truncated
// check that is the cap, not the true total, which is deliberately
// not computed.
TruncatedChecks []string `json:"truncatedChecks,omitempty"`
}
APIAnalysisResult is the JSON representation of analysis results.
type APIDefaultOverride ¶
type APIDefaultOverride struct {
Types []string `json:"types"`
Defaults map[string]string `json:"defaults"`
RelationDefaults map[string]string `json:"relationDefaults"`
}
APIDefaultOverride is the JSON representation of a default override.
type APIIssue ¶
type APIIssue struct {
EntityID string `json:"entityId"`
EntityType string `json:"entityType"`
Title string `json:"title,omitempty"`
Message string `json:"message"`
Severity string `json:"severity"` // "error" or "warning"
CheckType string `json:"checkType"`
// Detail carries optional structured specifics about why the issue
// fired, beyond the flat Message. For content required-headers
// violations it holds the missing exact headers. Absent (omitempty)
// on rows with no structured detail; the frontend reveals it in an
// expandable detail row under the message.
Detail []string `json:"detail,omitempty"`
// ScriptError carries the structured Lua-failure envelope for
// validation script-error rows. Absent (omitempty) on every
// other row. The frontend uses presence as the discriminator:
// rows with scriptError open the ScriptErrorDialog instead of
// navigating to an entity. Same loopback gating as the
// action-surface envelope (security.AllowFullScriptDetail).
ScriptError *ScriptErrorEnvelope `json:"scriptError,omitempty"`
}
APIIssue is the JSON representation of a single analysis issue.
type APIPropertyDef ¶
type APIPropertyDef struct {
Name string `json:"name"`
Type string `json:"type"`
Values []string `json:"values"`
}
APIPropertyDef describes a property for the settings page.
type APIRelationDef ¶
type APIRelationDef struct {
Name string `json:"name"`
Label string `json:"label"`
TargetType string `json:"targetType"`
Targets []APIRelationTarget `json:"targets"`
}
APIRelationDef describes a relation for the settings page.
type APIRelationTarget ¶
APIRelationTarget is a possible target for a relation.
type APISettingsData ¶
type APISettingsData struct {
UserDefaults APIUserDefaults `json:"userDefaults"`
UserPalette *dataentryconfig.PaletteConfig `json:"userPalette,omitempty"`
AllProperties []APIPropertyDef `json:"allProperties"`
AllRelations []APIRelationDef `json:"allRelations"`
EntityTypes []string `json:"entityTypes"`
// LogoURL is the cache-busted URL of the user-uploaded sidebar logo,
// or nil when no logo is set. The SPA reads this on boot to render
// the sidebar branding.
LogoURL *string `json:"logoUrl,omitempty"`
}
APISettingsData contains all data needed for the settings page.
type APIThemeImportResponse ¶
type APIThemeImportResponse struct {
Palette dataentryconfig.PaletteConfig `json:"palette"`
LogoURL string `json:"logoUrl,omitempty"`
}
APIThemeImportResponse is the typed shape of POST /_theme/import. Field names mirror the analogous fields in APISettingsData so the frontend stays aligned with the rest of the JSON surface.
type APIUserDefaults ¶
type APIUserDefaults struct {
Defaults map[string]string `json:"defaults"`
RelationDefaults map[string]string `json:"relationDefaults"`
Overrides []APIDefaultOverride `json:"overrides"`
}
APIUserDefaults is the JSON representation of user defaults.
type AffordanceDenialError ¶
type AffordanceDenialError struct {
Rule AffordanceDenialRule
Path string // property name, relation type, or "<relation-type>.<meta-field>"
Reason string
// Attribution names the role/grant that produced the deny, for the
// audit Summary channel (DR-C5). Empty for resolvers that don't
// track it. Never serialized to the wire 403 body.
Attribution string
}
AffordanceDenialError reports why a write was rejected by the affordance validator. The rule and path together form the wire rule_id (e.g. "field-affordance:hidden:priority"). Reason is a short human-readable explanation; UIs surface it as-is.
func (AffordanceDenialError) Error ¶
func (d AffordanceDenialError) Error() string
Error makes AffordanceDenialError satisfy the error interface so it can flow back through caller chains. The format mirrors RuleID() plus the reason.
func (AffordanceDenialError) RuleID ¶
func (d AffordanceDenialError) RuleID() string
RuleID returns the wire-stable identifier for this denial.
type AffordanceDenialRule ¶
type AffordanceDenialRule string
AffordanceDenialRule is the stable identifier surfaced in 403 responses when an affordance validator rejects a write. The full rule_id on the wire is "<rule>:<path>" so a UI or audit reader can reconstruct what was denied.
Rule names are part of the wire contract — changing them is a wire break.
const ( RuleFieldHidden AffordanceDenialRule = "field-affordance:hidden" RuleFieldReadOnly AffordanceDenialRule = "field-affordance:read-only" RuleFieldEnumFiltered AffordanceDenialRule = "field-affordance:enum-filtered" RuleRelationNotCreatable AffordanceDenialRule = "relation-affordance:not-creatable" RuleRelationNotRemovable AffordanceDenialRule = "relation-affordance:not-removable" RuleRelationMetaReadOnly AffordanceDenialRule = "relation-affordance:meta-read-only" )
type AffordanceProfile ¶
type AffordanceProfile string
AffordanceProfile names a verdict-source preset. "none" is the permissive default; "demo" is a fixture against the ticket type exercising every affordance code path. Absent an explicit profile, a policy carrying affordance grants selects the policy-backed resolver.
The env var RELA_AFFORDANCE_PROFILE selects the override at startup. cmd/rela-server and cmd/rela-desktop parse the var and pass the resolver into NewApp; tests pass the resolver directly.
const ( AffordanceProfileNone AffordanceProfile = "none" AffordanceProfileDemo AffordanceProfile = "demo" )
type AnalysisIssue ¶
type AnalysisIssue struct {
EntityID string // Empty for non-entity issues (e.g., ID gaps)
EntityType string
Title string
Message string
Severity string // "error" or "warning"
// Detail carries optional structured specifics about why the issue
// fired, beyond the flat Message. For content required-headers
// violations it holds the missing exact headers. Nil for issues
// with no structured detail.
Detail []string
// ScriptError carries the raw *lua.ScriptError for validation
// rules whose Lua script failed. Non-nil only on script-error
// rows; the HTTP handler converts it to a wire envelope using
// the per-request loopback gate, so the structured detail
// (path, source slice, stack) reaches the frontend's existing
// ScriptErrorDialog rather than a flat string.
// LoadErrors do NOT get a ScriptError — they're not Lua failures.
ScriptError *lua.ScriptError
}
AnalysisIssue represents a single validation issue, optionally linked to an entity.
type AnalysisResult ¶
type AnalysisResult struct {
Sections []AnalysisSection
ErrorCount int
WarningCount int
}
AnalysisResult is the complete output of running all analyses.
type AnalysisSection ¶
type AnalysisSection struct {
Name string
Description string
Issues []AnalysisIssue
// Truncated reports that the analyzer found MORE issues than it
// returned, so the UI can say so.
//
// Surfaced rather than silent on purpose. An operator who fixes all
// 100 reported issues, re-runs, and sees 100 again would otherwise
// conclude analyze is broken. The exact total is deliberately NOT
// reported: counting every issue means doing all the work the cap
// exists to avoid, and "100+" is as actionable as "12,431".
Truncated bool
}
AnalysisSection groups issues by analysis category.
func (AnalysisSection) ErrorCount ¶
func (s AnalysisSection) ErrorCount() int
ErrorCount returns the number of error-severity issues in this section.
func (AnalysisSection) WarningCount ¶
func (s AnalysisSection) WarningCount() int
WarningCount returns the number of warning-severity issues in this section.
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is the central application struct for the data-entry server.
Concurrency model ¶
The co-derived reload core (config, metamodel, style map, OpenAPI generator) lives in an immutable Schema published via [schemaProvider]'s atomic.Pointer. Handlers call a.State() once at entry and work against a coherent snapshot for the duration of the request — no lock acquisition, no risk of observing a half-reloaded world. Independently-owned reloadable state (logo, palette, user defaults) lives in its own self-synchronized service, not the snapshot.
Reloads (triggered by the file watcher or by Reload) derive a new Schema and publish it atomically via a.schema.Reload. The previous snapshot is garbage-collected once no reader holds it.
Mutations (CreateEntity, UpdateEntity, DeleteEntity, CreateRelation, UpdateRelation, DeleteRelation, SetProperty, action scripts) serialize via writeMu. writeMu excludes concurrent mutations but does NOT block readers — readers go through a.State(). The workspace's internal reloadMu coordinates the reload itself with the mutation path.
TODO(TKT-R68TV8): App is a god-object. Decompose toward the 40-method load line — extract the API/serialization/relation services into their own types. Ratchet this number DOWN as methods move out; never up EXCEPT for a new required route handler (App owns one method per registered HTTP route by the router's design). The sync route cluster (16 methods) moved to syncHandler (170 → 154); the command cluster (11 methods) moved to commandHandler (154 → 143); the attachment cluster (12 methods) moved to attachmentHandler / package functions (143 → 131); the write nucleus — entity/relation CRUD, clone, conflict-resolve, and the modern relations reconciler (18 methods) — moved to writeHandler (131 → 114); the Lua action handler joined it (115 → 114, from a base that had absorbed the DEC-O59WM4 script-read helpers), completing the write surface: every writeMu write path now lives on or routes through writeHandler. The views cluster — view traversal, section building, the /_views, /_sidepanel, /_sidebar handlers, and the form-resolution helpers (16 methods) — moved to viewsHandler, three receiver-free helpers became package functions, and the dead ungated server-rendered nav path (5 methods, #1043) was deleted (114 → 90).
The directive lags the real count (TKT-N0IKN9 tracks decomposing App); it is a ratchet target, not a budget to spend. SetUserState took it from 98 to 99 — it follows the existing SetSecurityConfig / SetJWTGate setter idiom rather than becoming a 12th positional NewApp parameter — and the CalDAV alias setter that landed alongside it took the count to 100, a later one to 101, and redactedForSuggestion to 102 — the field-redaction seam the next-action candidate path needs, which has to reach affordanceService.
TKT-BDG8U9 adds App.SetRemoteMCP on the same terms — the public opt-in setter for the remote MCP endpoint, matching that setter idiom. The rest of that feature deliberately stays OFF App: `registerMCPRoute` takes its handler as a parameter and `toolForPath` is a package function, so the mount cost one method rather than three.
TKT-N8XQ2R adds App.SetNextActionMatchers on the same terms (103 -> 104): the predicate compiler backing a source's `condition:` lives above this package, so it arrives through the same setter idiom rather than a 13th positional NewApp parameter. The compiling and matching themselves stay OFF App entirely — conditionlint owns them and appbuild bridges — so the feature cost one method, not a subsystem. TKT-DN37J2 adds App.SetWorlds on the same terms, and the same discipline held for the rest: `resolveWorld`, `attachWorld` and `worldCapablePath` are all package functions taking what they need, so request-level world selection cost ONE method rather than four.
The theme/settings/palette cluster (12 methods — /_theme/logo CRUD, the /_theme export/import pair, and the /_settings + /_palette CRUD) moved to appearanceHandler (TKT-8AJ1PM, 104 → 92).
The search-query pipeline (5 methods — executeQuery and its free-text branch, the list `?q=` id-set helper, and the sort / property-filter passes they share) moved to queryService, and the dead isRelationLinked was deleted (TKT-SJ0LRS, 92 → 86).
TKT-WRLDAPI item 4 (world-scoped relations) cost ZERO methods, and that was not free discipline — it was written as five methods first and plimsoll failed the build. They became package functions taking their seams explicitly (`worldOutgoingForEntity`, `worldNeighborsForPage`, `includeCandidates`, `defaultWorldCandidates`, `worldCandidates`, plus `SetWorldNeighbors`), which reads better anyway: the seams a world-scoped link read depends on are named in the signature rather than reached through this struct. The load line doing its job is worth recording, because the habit it interrupts is the one that produced the pre-extraction 104.
The merge of FEAT-9CD2MX with develop adds SetWorlds on top of develop's extractions (86 → 87). Neither side grew it carelessly — the worlds side cost exactly one method and said why above — but the integration is where the count actually moves, so it is recorded here rather than in either branch.
func NewApp ¶
func NewApp( fs storage.FS, paths *project.Context, meta *metamodel.Metamodel, st store.Store, versions store.VersionService, em *entitymanager.Manager, searcher search.Searcher, visibleSearcher search.VisibleSearcher, aclImpl acl.ACL, fieldResolver FieldVerdictResolver, auditSink audit.Audit, stateKV state.KV, ) (*App, error)
NewApp creates and initializes an App. Callers pass in the primitives (fs, paths, meta, store) plus the services that depend on workspace assembly: entityManager (the production write path) and searcher (the live Bleve index). Everything else — state.KV, config.Loader, tracer, templater, validator — is constructed locally.
The store-level file watcher (live-reload of external entity / relation edits) is feature-detected on `st` inside App.StartWatching via the [storeWatcher] interface; callers do not wire it.
func (*App) Cfg ¶
Cfg returns the current data-entry config (convenience accessor). Equivalent to a.State().Cfg.
func (*App) NewRouter ¶
NewRouter returns an http.Handler with all data entry routes registered. The Vue SPA serves as the primary UI at the root path.
When adding a route, add a probe to the route table in router_walk_test.go so registration stays covered.
func (*App) ProjectName ¶
ProjectName returns the display name of the loaded project.
func (*App) ProjectRoot ¶
ProjectRoot returns the root directory of the loaded project.
func (*App) SetCalDAVAliases ¶
func (a *App) SetCalDAVAliases(s *caldavalias.Service)
SetPrincipalResolver installs a custom PrincipalResolver used by the router's audit-stamp middleware. Must be called before App.NewRouter; subsequent changes have no effect on already-built routers.
The typical wiring (in cmd/rela-server) chains EnvPrincipalResolver and HeaderPrincipalResolver so a `$RELA_DATAENTRY_USER` env var overrides any incoming header and the header itself overrides the default. Passing nil restores [defaultPrincipalResolver] behavior. SetCalDAVAliases installs the CalDAV alias service. Without it the CalDAV routes are not registered.
func (*App) SetJWTGate ¶
func (a *App) SetJWTGate(cfg JWTGateConfig) error
SetJWTGate enables fail-closed verified-JWT identity. Must be called before App.NewRouter; subsequent changes have no effect on already-built routers.
When set, every [isAPIPath] request must carry an assertion that verifies, or it is denied 401 — see [requireVerifiedJWT]. This REPLACES the principal resolver for API requests rather than layering over it: JWT identity is exclusive, so there is no header or env source to fall back to. Callers must not also install a header/env chain via App.SetPrincipalResolver; cmd/rela-server enforces that at startup.
Returns an error when a required field is missing rather than accepting a config that cannot work. An empty HeaderName is fatal in a quiet way — every assertion would read as absent, so the server would boot clean and then 401 every API request.
Note the interface-nil caveat: a TYPED nil (e.g. a (*jwtauth.Verifier)(nil) stored in the interface) is not == nil and cannot be caught here without reflection. Callers must not construct one; cmd/rela-server checks the concrete pointer before it ever reaches this interface.
func (*App) SetNextActionMatchers ¶
func (a *App) SetNextActionMatchers(fn NextActionMatcherFunc) error
SetNextActionMatchers injects the predicate compiler backing a source's `condition:`.
Separate from NewApp for the same reason as App.SetUserState: the compiler lives above this package, so the composition root supplies it rather than this package importing it. Rejects nil for the same reason too — a silently absent compiler would leave every condition unevaluated, showing suggestions for entities the operator explicitly excluded.
func (*App) SetPrincipalHeader ¶
SetPrincipalHeader records the name of the HTTP header that carries the principal identity so API responses can declare `Vary` on it. Call alongside App.SetPrincipalResolver (before App.NewRouter) when wiring a HeaderPrincipalResolver; leave unset otherwise.
func (*App) SetPrincipalResolver ¶
func (a *App) SetPrincipalResolver(r PrincipalResolver)
func (*App) SetRemoteMCP ¶
func (a *App) SetRemoteMCP(factory MCPHandlerFactory) error
SetRemoteMCP enables the remote MCP endpoint, which is OFF by default.
It refuses a configuration that cannot be served safely, at startup, rather than at first request:
- a nil factory has nothing to serve;
- a factory that errors means the MCP wiring is broken;
- **no JWT gate is refused outright.** The endpoint needs a CSRF exemption (a non-browser MCP client sends no Origin), and that exemption is only sound while rela itself verifies a bearer token and requires it. In header-identity mode `requireVerifiedJWT` is never wrapped and the terminal resolver yields `User: "unknown"` — combining that with the exemption would publish an unauthenticated remote write surface. A declarative-ACL deployment would still fail closed (`acl.ErrUnstampedPrincipal` rejects `unknown`), but a NopACL deployment would not, and this must not depend on a second, unrelated setting.
The same reasoning as `validateIdentityFlags` in cmd/rela-server: an auth downgrade happens per request, long after anyone reads a startup warning, so it is refused rather than warned about.
Must be called before App.NewRouter.
func (*App) SetSecurityConfig ¶
func (a *App) SetSecurityConfig(cfg SecurityConfig) error
SetSecurityConfig configures the HTTP security middlewares applied by NewRouter. It must be called before NewRouter.
func (*App) SetUserState ¶
SetUserState replaces the next-action per-user state backend.
Defaults to an in-memory store (see NewApp), which is correct for a single-process deployment: this state is disposable, so losing it on restart costs a user one repeated suggestion. A deployment that wants snoozes to survive a restart, or to be shared across processes, injects a persistent backend here.
Rejects nil rather than quietly disabling the feature: a silently absent store would make the app stop honoring snoozes and mutes, which users experience as the system ignoring them — the deferred-failure symptom the project's constructor rule exists to prevent.
func (*App) SetWebhookReceiver ¶
SetWebhookReceiver enables the POST /webhooks/idp endpoint: a verified IdP callback that dispatches to the named action (which fetches authoritative user data and upserts a person entity). Must be called before NewRouter. A nil verifier or empty actionID leaves the receiver disabled and the route unmounted — matching the inert-when-unconfigured shape of the other optional wiring (SetPrincipalResolver, SetSecurityConfig).
func (*App) SetWorlds ¶
func (a *App) SetWorlds(w WorldLookup)
SetWorlds injects the compiled world map, enabling `?world=` selection.
Until this is called the App serves the default world only and REFUSES any `?world=` naming something else — which is the correct posture for a surface whose wiring never opted in, and means a deployment that does not use worlds cannot accidentally acquire the parameter.
func (*App) StartGitFetch ¶
func (a *App) StartGitFetch() (stop func())
StartGitFetch begins periodic git fetch in the background. Returns a stop function to shut down the fetcher.
coverage-ignore-func: background goroutine with timer
func (*App) StartWatching ¶
StartWatching begins file watching for live-reload. It does two independent things:
- Subscribes to `data-entry.yaml` via the config loader so SPA reloads pick up dashboard / palette / form config changes.
- Asks the store to start watching its own entity/relation files (feature-detected via [storeWatcher] — fsstore implements it; in-memory backends don't). The store's observer chain handles reindex automatically; no callback wiring is needed at this layer.
The returned error covers only the config-subscriber failure (step 1). Store-watcher errors (step 2) are logged via slog.Warn because store watching is a live-reload nice-to-have, not a startup requirement.
Stop via App.StopWatching. Note: that only releases the config subscription — the store-watcher lifecycle is owned by the store (closed when the store is closed).
coverage-ignore-func: requires real filesystem events via fsnotify
func (*App) State ¶
State returns the current reloadable Schema snapshot. Handlers should call State() once at entry and use the returned snapshot consistently throughout, instead of making multiple calls that could see different snapshots after a concurrent reload.
func (*App) StopWatching ¶
func (a *App) StopWatching()
StopWatching releases the data-entry.yaml subscription started by App.StartWatching. The store-level watcher (when present) has its own lifecycle managed by the store and is stopped during store close, not here — asymmetric on purpose: dataentry doesn't own the store, only its config subscription. StopWatching is lifecycle-only and must be called from a single goroutine (it is the StartWatching counterpart). The stop fields are not synchronized; concurrent Start/Stop is not supported.
type AppConfig ¶
type AppConfig = dataentryconfig.AppConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type AssertedIdentity ¶
type AssertedIdentity struct {
Subject string
OrgID string
OrgSlug string
Roles []string
// PrincipalType and Scopes drive client attenuation (TKT-IAC8TX): the
// former selects a ceiling in acl.yaml, the latter re-opens pieces of it.
// Both are absent for a proxy that doesn't model them, which means "no
// ceiling applies" — the principal keeps its acting user's grants.
PrincipalType string
Scopes []string
// Email is the verified `email` claim, when the proxy supplies one. Absent
// for a proxy that doesn't model it, which is not an error. Threaded onto the
// Principal so lazy provisioning (TKT-ANUJDS) can stamp it on a stub user
// entity; nothing in the ACL evaluates it.
Email string
}
AssertedIdentity is the verified-assertion payload this package consumes. It is dataentry's OWN type, not the verifier's: the wiring site adapts whatever the concrete verifier returns into this shape (see the adapter in cmd/rela-server), so dataentry never imports the verifier package and the verifier stays an arch-lint leaf.
Subject is the only field callers may treat as required. The rest are absent for any proxy that doesn't model orgs or roles, which is not an error.
type Calendar ¶
type Calendar = dataentryconfig.Calendar
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type CalendarSource ¶
type CalendarSource = dataentryconfig.CalendarSource
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type CommandConfig ¶
type CommandConfig = dataentryconfig.CommandConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type CommandMessage ¶
type CommandMessage struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Level string `json:"level,omitempty"`
Path string `json:"path,omitempty"`
Label string `json:"label,omitempty"`
Action string `json:"action,omitempty"`
ID string `json:"id,omitempty"`
EntityType string `json:"entity_type,omitempty"`
URL string `json:"url,omitempty"`
}
CommandMessage is a structured message parsed from a command's stdout.
type CommandScope ¶
type CommandScope = dataentryconfig.CommandScope
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type Config ¶
type Config = dataentryconfig.Config
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ConfigValidationError ¶
type ConfigValidationError = dataentryconfig.ConfigValidationError
ConfigValidationError is re-exported from dataentryconfig.
type DashboardCard ¶
type DashboardCard = dataentryconfig.DashboardCard
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type DashboardConfig ¶
type DashboardConfig = dataentryconfig.DashboardConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type DefaultOverride ¶
type DefaultOverride = dataentryconfig.DefaultOverride
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type DemoFieldVerdictResolver ¶
type DemoFieldVerdictResolver struct{}
DemoFieldVerdictResolver applies a fixed fixture against the "ticket" entity type. The fixture is hand-picked to exercise every affordance code path:
- kind: read-only (writable=false)
- priority: hidden (visible=false)
- effort: option-filtered ({l: false, xl: false})
- status: option-filtered ({done: false})
- affects relation: not creatable
- implements relation: not removable
- has-planning relation: meta-field "note" not writable (the metamodel doesn't currently declare relation-meta on this type, so the verdict is still emitted and the contract tests rely on a test-fixture metamodel that adds the meta field)
Other entity types receive empty verdicts. Intended for dev / manual-testing use only — the predicate ticket replaces this with a policy-driven resolver.
func (DemoFieldVerdictResolver) FieldVerdicts ¶
func (DemoFieldVerdictResolver) FieldVerdicts(_ context.Context, e *entityPkg.Entity) FieldVerdicts
FieldVerdicts returns the demo fixture for ticket entities and the zero value for every other type.
func (DemoFieldVerdictResolver) RelationVerdicts ¶
func (DemoFieldVerdictResolver) RelationVerdicts(_ context.Context, e *entityPkg.Entity) RelationVerdicts
RelationVerdicts returns the demo relation fixture for ticket entities and the zero value for every other type.
type Direction ¶
type Direction = dataentryconfig.Direction
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type DocumentConfig ¶
type DocumentConfig = dataentryconfig.DocumentConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type DocumentResult ¶
type DocumentResult struct {
// HTML is the rendered HTML content.
HTML string
// ContentHash is the hash of source entities used for cache validation.
ContentHash string
// Entities contains all entities involved in the document (for dependency tracking).
Entities []*entity.Entity
}
DocumentResult holds the result of rendering a document.
type EnumHelp ¶
type EnumHelp struct {
Property string
TypeName string
Initial string // entry state for the diagram; "" when unknown
Values []ValueHelp
Transitions []TransitionHelp
}
EnumHelp documents an enum/state-machine property's allowed values and (when it is a state machine) its lifecycle. Property is the property name, TypeName the custom-type name (empty for an inline enum). Values / Transitions are only populated when present; both may be empty for a plain field, in which case the property contributes no help sections (TKT-DUQBD0).
type FieldVerdictResolver ¶
type FieldVerdictResolver interface {
FieldVerdicts(ctx context.Context, e *entityPkg.Entity) FieldVerdicts
RelationVerdicts(ctx context.Context, e *entityPkg.Entity) RelationVerdicts
}
FieldVerdictResolver decides per-entity affordances for fields, enum options, and relation-meta fields. The wire shape it feeds into is documented in docs/data-entry/api-reference.md.
v1 ships two implementations:
- NopFieldVerdictResolver — returns zero verdicts; every field, option, and relation is permitted. Default unless RELA_AFFORDANCE_PROFILE selects another.
- DemoFieldVerdictResolver — a hardcoded fixture against the ticket type, exercising every affordance code path so the SPA work in TKT-G7N5 has an observable end-to-end behavior to test against.
The eventual predicate-engine ticket replaces both with a policy-driven implementation that reads acl.yaml. The interface shape is intentionally narrow so the swap is mechanical.
func ResolverFromProfile ¶
func ResolverFromProfile( profile string, meta *metamodel.Metamodel, st store.Store, declarative *acl.Declarative, ) (FieldVerdictResolver, error)
ResolverFromProfile returns the FieldVerdictResolver for the given profile, policy, and metamodel. Selection order (DR-M3):
- profile == "demo" → DemoFieldVerdictResolver (hard override, for dev / e2e fixtures even when a policy is present).
- profile == "none" → NopFieldVerdictResolver (hard opt-out).
- profile == "" and the policy declares any affordance grants → the policy-backed resolver.
- otherwise → NopFieldVerdictResolver.
An unknown profile logs a warning and falls back to step 3/4. A policy-backed resolver whose predicates fail to compile returns an error — the caller fails startup loudly (DR-M4), matching the acl.yaml hard-fail posture for genuinely broken config.
func ResolverFromServices ¶
func ResolverFromServices(svc ResolverServices) (FieldVerdictResolver, error)
ResolverFromServices builds the affordance resolver for an entry point, reading RELA_AFFORDANCE_PROFILE from the environment and the metamodel / store / declarative from svc. Both cmd entry points call this; they differ only in how they handle the returned error (rela-server exits, rela-desktop surfaces it to the UI), so error handling stays at the call site.
type FieldVerdicts ¶
type FieldVerdicts struct {
// Writable maps fieldName → writable. Absence = writable.
Writable map[string]bool
// Visible maps fieldName → visible. Absence = visible. False means
// the property is omitted from the wire `properties` map AND from
// `_fields`; the SPA's filter never sees the key.
Visible map[string]bool
// Options maps fieldName → optionValue → allowed. Absence of the
// field OR absence of an option means allowed. Used for enum-typed
// properties.
Options map[string]map[string]bool
// Attribution maps a denied path (field name, or "field=option")
// to the role/grant that produced the deny. Audit-only — never
// serialized to the wire. Sparse: only denials appear. Empty for
// resolvers (Nop / Demo) that don't track attribution.
Attribution map[string]string
}
FieldVerdicts carries per-entity field-level affordance decisions. All maps use sparse semantics: absence of a key means "default" (the permissive default — writable, visible, all options allowed). Only deviations need to be populated.
func (FieldVerdicts) IsOptionAllowed ¶
func (v FieldVerdicts) IsOptionAllowed(name, opt string) bool
IsOptionAllowed reports whether option `opt` is allowed for the enum-typed field `name`. The default is allowed — absent or true-valued entries both yield true; only explicit false values filter the option out.
func (FieldVerdicts) IsVisible ¶
func (v FieldVerdicts) IsVisible(name string) bool
IsVisible reports whether name is visible. The default is true — absent or true-valued entries both yield true; only explicit false values hide the field.
func (FieldVerdicts) IsWritable ¶
func (v FieldVerdicts) IsWritable(name string) bool
IsWritable reports whether name is writable. The default is true — absent or true-valued entries both yield true; only explicit false values are denials.
type FilterConfig ¶
type FilterConfig = dataentryconfig.FilterConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type FilterControl ¶
type FilterControl = dataentryconfig.FilterControl
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type Form ¶
type Form = dataentryconfig.Form
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type FormField ¶
type FormField = dataentryconfig.FormField
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type FormRelation ¶
type FormRelation = dataentryconfig.FormRelation
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type Gantt ¶
type Gantt = dataentryconfig.Gantt
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type GitStatusResponse ¶
type GitStatusResponse struct {
Available bool `json:"available"`
Branch string `json:"branch,omitempty"`
LocalChanges int `json:"local_changes"`
RemoteAhead int `json:"remote_ahead"`
Syncing bool `json:"syncing"`
Conflict bool `json:"conflict"`
ConflictFiles []string `json:"conflict_files,omitempty"`
}
GitStatusResponse is the JSON response for /api/git/status.
type GitSyncResponse ¶
type GitSyncResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
ConflictFiles []string `json:"conflict_files,omitempty"`
}
GitSyncResponse is the JSON response for /api/git/sync.
type GroupData ¶
type GroupData struct {
GroupName string
Rows []SectionRowData
Entities []SectionEntityData
}
GroupData holds a group of rows/entities for grouped table/card display.
type ImportedThemeAsset ¶
ImportedThemeAsset carries the bytes + sniffed extension of a logo extracted from a theme package. ImportedThemeAsset is nil when the package contained no logo (or didn't reference one).
type JWTGateConfig ¶
type JWTGateConfig struct {
// Verifier checks the assertion and projects its claims. Required.
//
// It returns the full [AssertedIdentity] (subject + org + roles), not just
// the subject, so the gate can stamp asserted roles onto the Principal it
// installs. A subject-only verifier here would authenticate correctly but
// silently strip every asserted role before the ACL sees it (TKT-OJL2GN).
Verifier assertionVerifier
// HeaderName is the request header carrying the assertion. Required.
HeaderName string
// unreachable (an operator-actionable outage) rather than the assertion
// being bad (a client fault). Both deny; they differ only in how they are
// logged.
//
// It is injected as a predicate rather than imported so this package stays
// independent of internal/jwtauth — the wiring layer supplies
// errors.Is(err, jwtauth.ErrKeysUnavailable). Optional: a nil predicate
// classifies every failure as a client fault.
KeysUnavailable func(error) bool
}
JWTGateConfig configures fail-closed verified-JWT identity. Install it with App.SetJWTGate; when set, App.NewRouter wraps the API surface in [requireVerifiedJWT] and the principal-resolver chain is bypassed entirely for those requests.
type Kanban ¶
type Kanban = dataentryconfig.Kanban
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type KanbanCard ¶
type KanbanCard = dataentryconfig.KanbanCard
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type KanbanColumn ¶
type KanbanColumn = dataentryconfig.KanbanColumn
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type KanbanSwimlane ¶
type KanbanSwimlane = dataentryconfig.KanbanSwimlane
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type List ¶
type List = dataentryconfig.List
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ListColumn ¶
type ListColumn = dataentryconfig.ListColumn
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type MCPHandlerFactory ¶
MCPHandlerFactory builds the MCP HTTP handler.
It returns a plain http.Handler, NOT an SDK server type: `internal/mcp` is the only component permitted to import the MCP go-sdk (arch-lint's `mcpgo` vendor grant), so the SDK type must not appear in this package's API. The wiring site owns the SDK entirely — protocol version, stateless mode, transport — and hands back something this package can serve.
It is called ONCE at router construction, not per request. Per-request state (the verified principal, the ACL Request) travels on the request ctx, which the middleware chain has already populated by the time the returned handler runs; the handler resolves it per call. A per-request factory would rebuild the whole tool registry on every message for no benefit.
Returning an error refuses to build the router at all, so a broken MCP wiring is a startup failure rather than a per-request 500 discovered later.
type NavigationEntry ¶
type NavigationEntry = dataentryconfig.NavigationEntry
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type NextActionMatcherFunc ¶
type NextActionMatcherFunc func( cfg *dataentryconfig.Config, meta *metamodel.Metamodel, ) (func(sourceID string) (nextaction.Matcher, bool), []string)
NextActionMatcherFunc compiles the `condition:` of every configured source against the current metamodel, returning a per-source lookup plus one message per problem.
The consumer-side seam for the predicate compiler: this package must not import it (arch-lint keeps the condition/policy engine above the data-entry app), so the composition root supplies an implementation.
type NopFieldVerdictResolver ¶
type NopFieldVerdictResolver struct{}
NopFieldVerdictResolver returns empty verdicts for every entity. computeFields / computeRelations interpret empty verdicts as "no deviations from default" and emit sparse `_fields: {}` and `_relations: {}` on the wire. The SPA renders unchanged.
func (NopFieldVerdictResolver) FieldVerdicts ¶
func (NopFieldVerdictResolver) FieldVerdicts(context.Context, *entityPkg.Entity) FieldVerdicts
FieldVerdicts always returns the zero value.
func (NopFieldVerdictResolver) RelationVerdicts ¶
func (NopFieldVerdictResolver) RelationVerdicts(context.Context, *entityPkg.Entity) RelationVerdicts
RelationVerdicts always returns the zero value.
type PaletteColors ¶
type PaletteColors = dataentryconfig.PaletteColors
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type PaletteConfig ¶
type PaletteConfig = dataentryconfig.PaletteConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ParsedThemePackage ¶
type ParsedThemePackage struct {
Manifest *dataentryconfig.ThemeManifest
Logo *ImportedThemeAsset
}
ParsedThemePackage is the typed result of parsing a `.relatheme` upload. Manifest is always populated on success; Logo is non-nil only when the manifest referenced a logo and the bytes passed validation.
type PrincipalResolver ¶
PrincipalResolver maps an incoming HTTP request to the audit Principal that should be stamped on its context. Compose multiple resolvers via ChainResolvers to layer (e.g.) an env-var override over a header reader over the default.
func ChainResolvers ¶
func ChainResolvers(resolvers ...PrincipalResolver) PrincipalResolver
ChainResolvers returns a resolver that tries each supplied resolver in order and returns the first one whose User is non-empty. If no resolver yields a user, falls back to [defaultPrincipalResolver] (Tool=data-entry, User=unknown). Used by cmd/rela-server to layer env → header → default.
**Chain contract for resolver authors.** Return a zero principal.Principal (User=="") to signal fall-through. The chain advances on `p.User == ""` and *ignores* Tool — every data-entry resolver hard-codes Tool=ToolDataEntry today, so distinguishing on Tool would be cosmetic. If a future resolver needs to return a different Tool, give it a non-empty User too and the chain will honor both.
func EnvPrincipalResolver ¶
func EnvPrincipalResolver() PrincipalResolver
EnvPrincipalResolver reads Principal.User from $RELA_DATAENTRY_USER. Returns a zero principal when the env is unset or whitespace-only — chain it (typically first) so it acts as a local-dev escape hatch that overrides any incoming header.
The env var is read on *every* request rather than cached at construction so test fixtures using `t.Setenv` work without rebuilding the resolver. The cost is one map lookup per request (Go runtime takes a RLock); negligible relative to the per-request work of the audit middleware that follows.
Sanitization mirrors HeaderPrincipalResolver.
func HeaderPrincipalResolver ¶
func HeaderPrincipalResolver(headerName string) PrincipalResolver
HeaderPrincipalResolver reads Principal.User from headerName on each request, stamping Tool=data-entry.
The returned resolver is never nil. An empty headerName yields a resolver that always returns a zero Principal — the ChainResolvers composition relies on this shape, so callers don't need to special- case the disabled state. Production wiring in cmd/rela-server passes the raw flag value; the empty-default flag stays inert.
**Trust boundary.** The header value is only as trustworthy as the reverse proxy that sets it. Operators serving data-entry without a trusted proxy must not enable this resolver — anyone can spoof identity by setting the header on the wire. See docs/server-security.md for the deployment guidance.
Sanitization: control characters (C0 + DEL) in the header value are replaced with regular spaces, the value is truncated to 256 runes (UTF-8 safe), and surrounding whitespace is trimmed. Control-only values therefore sanitize to "" and fall through.
func JWTPrincipalResolver
deprecated
func JWTPrincipalResolver(v assertionVerifier, headerName string) PrincipalResolver
Deprecated: production wiring uses [requireVerifiedJWT] via App.SetJWTGate, which fails CLOSED. This resolver returns a zero Principal on a verification failure so a chain falls through to the next source — under a header chain that is an auth downgrade, which is why cmd/rela-server no longer wires it. Retained for callers embedding dataentry with their own chain semantics.
JWTPrincipalResolver reads a signed identity assertion from headerName, verifies it (ES256 against the proxy's JWKS, via v), and stamps the verified STABLE subject as Principal.User. This is provider-agnostic — any OIDC proxy that injects a signed JWT works by configuring the issuer/audience/JWKS/header.
Unlike HeaderPrincipalResolver (which trusts the proxy set the header), this resolver CRYPTOGRAPHICALLY verifies the assertion, so it is safe even when the header could reach the server from the network — a spoofed header without a valid signature simply fails verification and falls through.
Tool is principal.ToolDataEntry: the assertion changes WHO authenticated, not the entry point (a verified user still arrives via the data-entry HTTP surface).
It also carries the assertion's org and role claims onto the Principal via principal.Verified. This is the ONLY resolver that may do so — the header and env resolvers have no verified source for them, and a role reaching the ACL from a spoofable header would be a complete authorization bypass. The unexported fields on Principal make that structural rather than a convention.
A missing header, an "Authorization: Bearer <jwt>" wrapper (the scheme is stripped case-insensitively per RFC 6750), or any verification failure yields a zero Principal so the chain falls through — a nil v also yields an inert resolver. Empty headerName ⇒ inert (matches the disabled-flag shape of the other resolvers).
type PropertyHelp ¶
type PropertyHelp struct {
Name string
Type string
Required bool
Description htmltemplate.HTML
}
PropertyHelp holds documentation for a single property.
type RelationHelp ¶
type RelationHelp struct {
Name string
Label string
TargetType string // target type for outgoing, source type for incoming
Cardinality string
Required bool // true if min cardinality >= 1
Description htmltemplate.HTML
}
RelationHelp holds documentation for a single relation.
type RelationOp ¶
type RelationOp int
RelationOp identifies which relation-write operation a caller is gating. Pass via App.validateRelationOp.
const ( // RelationOpCreate gates adding an edge of the given type. RelationOpCreate RelationOp = iota // RelationOpRemove gates removing any edge of the given type. RelationOpRemove )
type RelationProperty ¶
type RelationProperty = dataentryconfig.RelationProperty
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type RelationVerdict ¶
type RelationVerdict struct {
Creatable bool
Removable bool
// Fields maps metaField → writable. Absence = writable. Applies
// uniformly to every link of this relation type (per-link
// affordances are predicate territory, deferred).
Fields map[string]bool
// Attribution maps a denied dimension ("create", "remove",
// "fields.<name>") to the role/grant that denied it. Audit-only,
// never serialized. Sparse.
Attribution map[string]string
}
RelationVerdict carries the affordance decision for a single relation type. Zero-value (Creatable=false, Removable=false, Fields=nil) would deny everything; callers always populate explicitly.
type RelationVerdicts ¶
type RelationVerdicts struct {
Types map[string]RelationVerdict
}
RelationVerdicts carries per-entity relation-level affordance decisions. The map is sparse: relation types not listed default to fully-permitted ({creatable: true, removable: true} with no meta-field restrictions).
type RelationVisibilityResolver ¶
type RelationVisibilityResolver interface {
RelationFieldVerdicts(
ctx context.Context, from *entityPkg.Entity, relType string, metaKeys []string,
) map[string]bool
}
RelationVisibilityResolver is the OPTIONAL sibling of FieldVerdictResolver that answers per-meta-field READ visibility for one relation edge (TKT-B1F5Q1). Kept separate and type-asserted (not embedded) for the same reason as TransitionResolver: only the policy-backed resolver can express a relation `visible:` grant, so the Nop and Demo resolvers don't implement it and relation meta is emitted un-redacted under them (matching how relations behaved before B1F5Q1). This mirrors the store's optional capabilities.
from is the source entity that owns the relation grant block; relType is the edge's relation type; metaKeys are the property names actually present on the edge about to be serialized (the closed-world deny universe). The result is sparse: an absent key means visible, a `false` value means hide that meta key.
type ResolvedCommand ¶
ResolvedCommand is a command that has been matched to a specific page context.
type ResolvedPalette ¶
type ResolvedPalette = dataentryconfig.ResolvedPalette
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ResolverServices ¶
type ResolverServices interface {
ACLDeclarative() *acl.Declarative
Meta() *metamodel.Metamodel
Store() store.Store
}
ResolverServices is the slice of [appbuild.Services] that ResolverFromServices needs. Declared here at the call site so the entry points pass `svc` directly without each re-spelling the env-read + accessors.
ACLDeclarative may return nil when no policy is wired (NopACL); the resolver constructor handles that case by selecting NopFieldVerdictResolver.
type Schema ¶
type Schema struct {
Cfg *Config
Meta *metamodel.Metamodel
StyleMap map[string]map[string]string
StyledTypes map[string]bool
OpenAPIGen *openapi.Generator
}
Schema is the co-derived reload core of the data-entry app: the config and metamodel, plus everything that is a pure function of the two (the style map and the OpenAPI generator). These MUST move together — the style map is derived from (Cfg, Meta), so publishing them independently could let a reader observe a new metamodel with a stale style map.
It is the residue of the former AppState after the independently-owned state (logo, palette, user defaults) moved into their own self-synchronized services. Readers Load a coherent Schema once per request via [schemaProvider.Current] (exposed on App as State()).
type ScopeDescriptor ¶
type ScopeDescriptor struct {
Source string `json:"source"` // "list" | "search"
Type string `json:"type"` // entity type name (singular)
Filters map[string]string `json:"filters,omitempty"` // filter[...] bracket keys → value
Sort string `json:"sort,omitempty"` // "-created,title" form
Q string `json:"q,omitempty"` // free-text query
}
ScopeDescriptor encodes the query that defines an ordered result set the user is navigating — a typed list, a search result, and (later) other sources. It rides on the wire as a single URL-encoded JSON `scope` param, unsigned: every field it carries is already freely issuable by the client against the list endpoint, so there is nothing to protect against tamper. Correctness comes from strict decoding in scopeFromParam, not from a signature. See issue #844 and docs/data-entry/api-reference.md.
Filters carries the same flat bracket-format keys the SPA already emits via filterStateToApiParams ("filter[status]", "filter[due][gte]", …). Reusing that wire format keeps a single source of truth for filter serialization and lets the descriptor rebuild a url.Values that the shared list pipeline consumes verbatim.
type ScriptErrorEnvelope ¶
type ScriptErrorEnvelope struct {
Error string `json:"error"`
CorrelationID string `json:"correlation_id,omitempty"`
Script ScriptIdentity `json:"script"`
Lua ScriptErrorLua `json:"lua"`
Source []lua.SourceLine `json:"source,omitempty"`
Stack []lua.StackFrame `json:"stack,omitempty"`
CapturedOutput string `json:"captured_output,omitempty"`
}
ScriptErrorEnvelope is the on-the-wire representation of a Lua script failure. The HTTP layer always returns this with status 422 so the frontend can branch on `error == "script_error"`. Every field except `correlation_id`, `script.path`, and `lua.message` is loopback-gated: non-loopback callers receive a degraded shape (see security.AllowFullScriptDetail).
JSON tags on lua.ScriptError are advisory: each consumer owns its wire shape. Data-entry uses this envelope; MCP marshals lua.ScriptError directly. Adding a field to lua.ScriptError requires deciding whether to mirror it here or let it surface only over MCP.
type ScriptErrorLua ¶
ScriptErrorLua is the message + line that always survives gating, so even a non-loopback caller knows roughly what broke without leaking the full source slice.
type ScriptIdentity ¶
type ScriptIdentity struct {
Surface string `json:"surface"`
Path string `json:"path"`
EntityID string `json:"entity_id,omitempty"`
Args map[string]any `json:"args,omitempty"`
}
ScriptIdentity carries who-was-running info: the surface (action, document, automation, lua_run, lua_eval), the script path, and any triggering entity / args context the surface decided to capture.
type SectionAddInfo ¶
type SectionAddInfo struct {
Relation string
LinkAs string // "from" or "to" — role of the new entity in the relation
PeerID string // entry entity ID
Targets []SectionAddTarget
}
SectionAddInfo describes an "Add" button on a view section.
type SectionAddTarget ¶
SectionAddTarget holds a possible entity type target for an "Add" button.
type SectionColumnData ¶
type SectionColumnData struct {
Values []string
PropType string
Widget string
Link string // resolved link URL or empty
EntityID string
EntityType string
}
SectionColumnData holds a resolved table cell for template rendering.
type SectionData ¶
type SectionData struct {
Heading string
SectionID string
Display string
Fields []SectionFieldData
Entities []SectionEntityData
Columns []ListColumn
Rows []SectionRowData
Groups []GroupData
IsGrouped bool
EmptyMessage string
IsEmpty bool
Link string // section-level link configuration (currently unused in templates)
Content string
HasContent bool
AddInfo *SectionAddInfo
LinkInfo *SectionLinkInfo
}
SectionData holds all resolved data for a single view section.
type SectionEntityData ¶
type SectionEntityData struct {
ID string
Title string
Type string
EditFormID string
Fields []SectionFieldData
Content string
HasContent bool
Props map[string]any
FieldVerdicts map[string]v1.FieldAffordance
// World is the face-provenance of this entity under the view's world
// (TKT-WRLDAPI item 4b). Nil under the default world.
World *v1.EntityWorld
}
SectionEntityData holds a resolved entity for template rendering.
`Props` and `FieldVerdicts` (TKT-IHC7D) carry the typed property values and per-cell writability verdicts for inline-edit hosts on cards/list view sections. Both are hidden-property-stripped. The wire converter dumb-copies them into v1.ViewEntity._props and v1.ViewEntity._fields respectively. They are nil for code paths that don't compute them (notably the entry-source branch and table rows); the wire converter's nil-checks gate emission.
type SectionFieldData ¶
type SectionFieldData struct {
Property string
Label string
Values []string
PropType string
Inaccessible bool
Span int
Render string
// Widget is the config's widget override for this field, empty when the
// author did not set one. Passed through verbatim: resolving it is the
// SPA's job (its registry owns the type→widget default), and the server
// has already rejected a name/type mismatch at config load (TKT-3R7RF3).
Widget string
}
SectionFieldData holds a single resolved field for template rendering. Values is always a list so that list-typed properties (list: true in the metamodel) retain per-item structure; scalar properties become a 1-element slice. Empty properties emit an empty slice.
Property is the raw property name (e.g. "title"); Label is its human-readable form. Inaccessible is true when the underlying entity is git-crypt encrypted and the value cannot be read with the current key — frontends render a lock indicator instead of the (absent) value. Span is the field's width on the 12-column layout grid, carried through from the view config. 0 means full width — the default for every auto-generated view, so a section with no spans authored renders as one scannable column.
Render is the resolved render mode ("display" | "input", TKT-HOIX1) — see dataentryconfig.ResolveFieldRender. Resolved server-side so the SPA never reimplements the section→field inheritance rule.
type SectionLinkInfo ¶
type SectionLinkInfo struct {
Relation string // relation type name
LinkAs string // "from" or "to" — role of the linked entity
PeerID string // entry entity ID
EntityTypes []string // valid target entity types
}
SectionLinkInfo describes a "Link existing" button on a view section.
type SectionRowData ¶
type SectionRowData struct {
EntityID string
EntityType string
EditFormID string
Cells []SectionColumnData
Content string
}
SectionRowData holds a resolved table row for template rendering.
type SecurityConfig ¶
type SecurityConfig struct {
// BindAddress is the host:port (or :port) the server is bound to.
// Used to derive the default Host and Origin allowlists.
BindAddress string
// AllowedOrigins are extra origins permitted in addition to the
// loopback defaults derived from BindAddress. Used to allow dev servers
// such as Vite running on a different port.
AllowedOrigins []string
}
SecurityConfig configures the HTTP security middlewares.
rela-server is intended to run on a local port, but a browser visiting any other site is already inside the loopback trust boundary. These middlewares reject:
- Requests whose Host header is not in the loopback allowlist (DNS rebinding defense).
- Requests to sensitive endpoints whose Origin (or Referer fallback) is not in the allowlist (CSRF / cross-origin read defense).
All sensitive endpoints are protected on every method, not just non-safe ones, because some handlers (e.g. /api/command/) historically accept GET for state-changing operations and a method-based filter would miss `<img src=...>` style attacks.
type Services ¶
type Services struct {
// Store provides entity/relation CRUD. Handlers use it for read
// operations; writes go through the workspace's EntityManager.
Store store.Store
// Tracer walks relations for trace/path/orphan queries.
Tracer tracer.Tracer
// Searcher runs free-text queries against the search index.
Searcher search.Searcher
// Meta is the current metamodel snapshot.
Meta *metamodel.Metamodel
}
Services bundles the backend services the data-entry handlers read from. Each consuming package keeps its own Services type — the coupling reduction is worth the field duplication with lua.WriteDeps and friends.
The bundle carries only what HTTP handlers actually need: read-side access to the store and tracer, free-text search, and the metamodel. Writes continue to flow through the entity manager so automations and validations fire.
type SidePanelConfig ¶
type SidePanelConfig = dataentryconfig.SidePanelConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type SortSpec ¶
type SortSpec = dataentryconfig.SortSpec
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type TransitionHelp ¶
type TransitionHelp struct {
Move string // the move label, falling back to the To value
From string
To string
Help htmltemplate.HTML
}
TransitionHelp documents one lifecycle move: the target label (the verb), the From→To states, and the optional Help prose (why/when to make the move).
type TransitionResolver ¶
type TransitionResolver interface {
TransitionVerdicts(ctx context.Context, e *entityPkg.Entity) map[string][]statemachine.TransitionVerdict
// EntryValues returns, per state-machine-typed property of entityType, the
// value a create must enter at (the machine's Initial/Default — BUG-X1C7S).
// The create form uses it to lock the field to its initial value. Empty when
// entityType has no machine-typed property.
EntryValues(entityType string) map[string]string
}
TransitionResolver is the OPTIONAL sibling of FieldVerdictResolver that answers state-machine transition verdicts for an entity (TKT-3G93B8). It is kept separate — and type-asserted, not embedded — because only the policy-backed resolver can answer it; the Nop and Demo resolvers don't implement it, so `_transitions` is simply absent under them (the SPA falls back to the ordinary enum control). This mirrors the store's optional capabilities (e.g. HistoryReader), which are also type-asserted rather than forced into the base interface.
The returned map is keyed by property name; each value is the resolved outgoing transitions for the ctx principal on e. An empty map means "no machine-typed property on this entity" (or no machines wired).
type UserDefaults ¶
type UserDefaults = dataentryconfig.UserDefaults
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ValueHelp ¶
type ValueHelp struct {
Value string
Label string
Description htmltemplate.HTML
}
ValueHelp documents one allowed value of an enum: the raw value, its optional display Label, and its optional prose Description (CustomType.Descriptions).
type ViewConfig ¶
type ViewConfig = dataentryconfig.ViewConfig
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ViewEntry ¶
type ViewEntry = dataentryconfig.ViewEntry
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ViewSection ¶
type ViewSection = dataentryconfig.ViewSection
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ViewSectionField ¶
type ViewSectionField = dataentryconfig.ViewSectionField
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type ViewTraverse ¶
type ViewTraverse = dataentryconfig.ViewTraverse
Config type aliases — re-exported from dataentryconfig for backward compatibility.
type Warning ¶
Warning is a type alias for entity.Warning so that handlers in this package can write `dataentry.Warning` without importing the entitymanager package at every call site. Behavior is identical.
type WebhookClaims ¶
type WebhookClaims struct {
Event string // the event name, e.g. "membership.created"
UserID string // the subject the event concerns
OrgID string // the tenant the event concerns
ID string // the webhook id (jti), for replay dedup
}
WebhookClaims is the verified subset of an inbound webhook the receiver acts on. It mirrors jwtauth.WebhookClaims but is declared HERE so the dataentry package needn't import jwtauth (the inward-pointing layering rule — the same reason the JWT gate takes a local assertionVerifier interface). The wiring layer, which may import both, adapts the concrete verifier to this shape.
type WorldLookup ¶
type WorldLookup interface {
Lookup(name string) (store.WorldScope, bool)
}
WorldLookup resolves a declared world NAME to its compiled scope.
Consumer-side interface: internal/dataentry may not import internal/worlds (arch-lint), and a store.WorldScope is metamodel-free by construction, so the compiled map is injected from the wiring site via App.SetWorlds.
It must FAIL CLOSED on an unknown name — returning ok=false rather than substituting the default world, which would silently widen a request that asked for a narrower view.
Source Files
¶
- actions.go
- affordances.go
- affordances_policy.go
- affordances_stub.go
- analyze.go
- api_v1.go
- app.go
- appearance_handler.go
- apps.go
- apps_css.go
- apps_editor.go
- apps_handler.go
- apps_sdk.go
- attachment_handler.go
- caldav_backend.go
- caldav_ctag.go
- caldav_handler.go
- caldav_ical.go
- caldav_mapping.go
- caldav_proppatch.go
- caldav_write.go
- capabilities.go
- command_handler.go
- commands.go
- config.go
- copies_handler.go
- custom.go
- custom_handler.go
- dashboard_handler.go
- default_view.go
- document.go
- entityreader.go
- entityserializer.go
- errors.go
- export.go
- export_list.go
- feed_handler.go
- feed_provider.go
- feed_uid.go
- filter_helpers.go
- gantt_handler.go
- handlers.go
- handlers_attachment.go
- handlers_document.go
- handlers_git.go
- handlers_theme.go
- handlers_theme_package.go
- helpers.go
- history_handler.go
- history_origin.go
- history_restore.go
- historyworld.go
- jwtgate.go
- logo_store.go
- mcp_http.go
- mentions.go
- middleware_security.go
- nextaction.go
- nextaction_handler.go
- palette_service.go
- provision.go
- queryservice.go
- readgate.go
- relation_history_handler.go
- relation_read_handler.go
- relation_visibility.go
- relations.go
- relations_direction.go
- relations_modern.go
- return_path.go
- router.go
- schema.go
- schemaworlds.go
- scope.go
- script_errors.go
- sections.go
- services.go
- settings_handlers.go
- settings_service.go
- standalone_document_handler.go
- static.go
- sync.go
- sync_handler.go
- theme_logo.go
- theme_package.go
- validate.go
- views.go
- views_handler.go
- viewworld.go
- visibility_adapters.go
- visiblereader.go
- watcher.go
- webhook.go
- webhook_routes.go
- world.go
- worldneighbors.go
- write_handler.go