- pact.FormFieldType gains RequestOptions, called with the application's database once per form schema request for each type the form uses - the form schema route merges the answer into a copy of each field's typeOptions; a fields.yaml key wins, the compiled schema never changes - an error logs one warning naming the type and serves the fields.yaml options only - TestFieldTypeRequestOptions covers every screen, the precedence, the call count, the error path, unchanged schemas, form extension fields and the authenticated handler; a runnable example backs the docs - the typeOptions description, the regenerated OpenAPI document and schema.d.ts, the field types page and the pact and cabana READMEs document it
249 lines
36 KiB
Markdown
249 lines
36 KiB
Markdown
# pact
|
|
|
|
Capability interfaces that compiled plugins implement to contribute routes, config, migrations, middleware, commands, admin screens, translations, mail templates, jobs and scheduled commands.
|
|
|
|
`import "git.golem15.com/golem15/summercms/modules/pact"`
|
|
|
|
**Name:** An agreement both sides keep; it holds the interfaces a plugin agrees to implement.
|
|
|
|
## Overview
|
|
|
|
`pact` is the contract layer between plugins and the framework. It holds interfaces and plain data types, with no behaviour of its own. A plugin opts into a capability by implementing one of the `Has*` interfaces, and the framework package that owns the capability discovers it with a type assertion ([party](../party/README.md) for config, [surf](../surf/README.md) for routes and middleware, [lagoon](../lagoon/README.md) for migrations, [cabana](../cabana/README.md) for admin controllers, [conga](../conga/README.md) for jobs and schedules). It replaces the `register*()` methods of a WinterCMS PluginBase (`registerPermissions`, `registerNavigation`, `registerSettings` and so on) with small, separately implementable interfaces.
|
|
|
|
## Role in the framework
|
|
|
|
The contract layer between the framework and plugins; it depends only on `bonfire`. `party`, `lagoon`, `conga`, `cabana`, `phrasebook`, `postcard`, `lighthouse` and `surf` all read it. It holds interfaces only, so a plugin can depend on the contracts without pulling in the modules that implement them.
|
|
|
|
Use cases:
|
|
|
|
- Make a plugin contribute routes, migrations, commands and admin screens by implementing the matching interfaces.
|
|
- Offer optional features: a plugin implements only the capabilities it needs.
|
|
- Write plugins that are compiled in without a runtime loader, checked by the compiler.
|
|
|
|
## Features
|
|
|
|
- Plugin capability interfaces: `pact.HasRoutes`, `pact.HasConfig`, `pact.HasMigrations`, `pact.HasCommands`, `pact.HasModels`, `pact.HasJobs`, `pact.HasSchedule`, `pact.HasLang`, `pact.HasLangOverrides` and `pact.HasMailTemplates`.
|
|
- HTTP contracts: the `pact.Router` group builder (implemented by surf), the `pact.Middleware` type, and named, parameterized (`name:param`) and house-envelope middleware through `pact.HasMiddleware`, `pact.HasMiddlewareFactories` and `pact.HasHouseMiddleware`.
|
|
- Backend registration data: `pact.Permission`, `pact.NavigationItem` and `pact.SettingsItem`, exposed through `pact.HasPermissions`, `pact.HasNavigation` and `pact.HasSettings`. A settings entry is either a singleton form or a link that opens an admin controller from the Settings page. A `pact.NavigationItem.Visible` func hides a navigation entry at request time (nil = always listed).
|
|
- Personal API token scopes: `pact.TokenScope` and `pact.HasTokenScopes`, so a plugin declares named scopes (permissions and HTTP methods) that administrators can assign when creating a personal API token.
|
|
- Member token scopes: `pact.FrontendTokenScope` and `pact.HasFrontendTokenScopes`, the frontend counterpart of `pact.HasTokenScopes`, so a plugin declares scopes for the user plugin's member personal API tokens; the admin token catalog never reads them. `pact.FrontendTokenScope.Implies` is only `""`, `read` or `write`, for a later OAuth MCP check; empty means the scope is not implied, and the route middleware is still what enforces a member scope. The plugin that owns member tokens publishes the compiled list as `pact.MemberTokenScopeCatalog` (`pact.MemberTokenScopeCatalog.MemberTokenScopes`). `pact.ValidTokenScopeName` is the one scope-name rule both catalogs apply.
|
|
- Frontend permission codes: `pact.FrontendPermission` and `pact.HasFrontendPermissions`, the frontend counterpart of `pact.HasPermissions`, so a plugin guarding a route with a permission code also declares that code for the user plugin's permission editors.
|
|
- MCP declarations, free of any MCP SDK: `pact.HasMCP` with `pact.MCP`, `pact.MCPTool` and `pact.MCPPrompt` bind tools and prompts to the plugin's own token-guarded routes; `pact.MCPGuard` and `pact.MCPGuards` let the owner of a route guard publish how its routes are guarded and dispatched; `pact.MCPTokenVerifier`, `pact.MCPPersonalTokenResolver` and `pact.MCPLinkedBackendResolver` are the services the MCP endpoint looks up; `pact.MCPAdvertisedScopes` (`pact.MCPAdvertisedScopes.MCPScopesSupported`) is the scope list that verifier publishes for protected-resource metadata, separate from `pact.MCPTokenVerifier` so that interface stays `MCPVerifyToken` and `MCPResource`; `pact.MCPSubjectChanged` is the event that refreshes a session's tool list. An in-process tool call carries a `pact.MCPCall`, stored with `pact.WithMCPCall` and read with `pact.MCPCallFrom` (absent on a plain HTTP request).
|
|
- My account contributions: `pact.HasAccountContributions` with `pact.AccountContribution` and `pact.AccountAction`, so a plugin adds a section with a status line and buttons to the administrator's own account page, run for the signed-in administrator only.
|
|
- Admin controller contracts: `pact.AdminController`, `pact.HasAdminControllers`, `pact.AdminAssets` (embedded Winter-shaped admin YAML), `pact.AdminPermissioned` and `pact.AdminRecordSource`.
|
|
- Admin extension contracts, so a plugin extends the compiled admin SPA without a Node build: `pact.AdminClientAssets` (per-controller JS and CSS from the plugin's embedded `assets/` tree, Winter's `addJs`/`addCss`), `pact.HasAdminActions` with `pact.AdminAction`, `pact.AdminActionInput` and `pact.AdminActionResult` (named toolbar and widget actions whose routes, CSRF check, permissions and record scoping the framework owns), `pact.HasAdminBulkActions` with `pact.AdminBulkAction`, `pact.AdminBulkActionInput` and `pact.AdminBulkActionResult` (named actions on the rows selected in a list, which receive records the framework loaded through the list scope, never ids; a `Detached` action runs outside the framework transaction), `pact.HasAdminRecordActions` with `pact.AdminRecordAction`, `pact.AdminRecordActionInput` and `pact.AdminRecordActionResult` (named actions on one record, each with an `Applies` rule for the record's state), `pact.HasAdminActionExtensions` with `pact.AdminActionExtension` (a plugin adds record actions to any admin controller by controller ID, one it does not own included; they are offered on that controller's preview screen after the owner's) and `pact.AdminFormExtension` (a plugin places fields and tabs in another controller's form, before or after the owner's, and loads and saves their values itself inside the record's save transaction), and `pact.AdminPartialData` (the curated view model a partial template renders).
|
|
- Plugin field types: `pact.HasFormFieldTypes` returns `pact.FormFieldType` entries, admin form field types any controller's `fields.yaml` uses by a short alias (`type: notes`). A type names its custom element, its scripts and styles, its own option keys and its Go funcs: `Decode` turns a submitted `pact.FormFieldValue` into a `pact.FormFieldDecoded` (the value to store plus optional `pact.FormFieldNotice` messages for the administrator, such as what a sanitizer removed) or refuses it with a `pact.FormFieldError`; `Load` serves the stored value to the element, `ListValue` formats a list column, `Upload` stores a file from a `pact.FormFieldUpload` and answers a `pact.FormFieldUploadResult`, and `RequestOptions` adds options that change per request (a site setting the element follows) to the served form schema. The admin framework owns the routes, permissions, record scope, locale storage of a translatable type and the localization of notices.
|
|
- Optional admin hooks a controller or model can implement: list and form query scoping (`pact.ListExtendQuery`, `pact.FormExtendQuery`), list row states (`pact.ListRowStates` with the fixed `pact.RowState` set `pact.RowStateDeleted`, `pact.RowStateNegative` and `pact.RowStateDisabled`), values of unstored list columns filled once per page (`pact.ListExtendRecords`), the record title of the form and preview screens (`pact.FormRecordTitle`), create, update and delete hooks (`pact.FormBeforeCreate`, `pact.FormAfterUpdate`, `pact.FormBeforeDelete` and their siblings), a hook after a reorder save (`pact.ReorderAfterSave`), form-only fields that reach those hooks without being model columns (`pact.FormVirtualFields`), validation rules per operation for admin saves (`pact.FormRules`), relation hooks (`pact.RelationExtendManageQuery`, `pact.RelationExtendOptionsQuery`, `pact.RelationBeforeLink`), relation child hooks around creating, updating and deleting a related record (`pact.RelationBeforeCreate`, `pact.RelationAfterCreate`, `pact.RelationBeforeUpdate`, `pact.RelationAfterUpdate`, `pact.RelationBeforeDelete`, `pact.RelationAfterDelete`), filter scopes (`pact.FilterScope`, `pact.FilterOptions`) and dropdown options (`pact.DropdownOptionsProvider`).
|
|
- Dependent dropdown options: `pact.DependentOptionsProvider` answers a `pact.DependentOptionSet` (whether the sibling's value takes a choice at all, and the choices) for a dropdown with `dependsOn`, from the submitted values of its siblings and with a context, so the choices can come from the database; the admin framework calls it for the options route and again on every save. `pact.Option.Depth` indents a choice taken from a tree.
|
|
- Menu item types: `pact.HasMenuItemTypes` returns `pact.MenuItemType` values (key, label, default URL pattern, `PatternFrom` for a list type, the `References` an item can point at as a `pact.MenuItemReference` tree, and `Resolve`), the typed successor of WinterCMS's `pages.menuitem.*` events. `Resolve` receives a `pact.MenuItemResolve` with the pattern already layered and answers a `pact.ResolvedMenuItem` with its alternates as an ordered `[]pact.MenuItemAlternate`. [signpost](../signpost/README.md) collects them at boot. A record type may set `Content` to a `pact.ContentDescriptor`, which describes its records for consumers that work on every content type at once (sitemaps, search indexers, metadata editors): the admin controller that edits them, their morph type, `Describe` (a `pact.ContentRecord` with per-locale `pact.ContentLocale` URL, title, description, image and word count), and the optional `Records` (paged through `pact.ContentList` and `pact.ContentPage`) and `Match` (a public path to a `pact.ContentMatchResult`).
|
|
- Content facts: a descriptor may also say what kind of record the type holds (`pact.ContentDescriptor.Kind`) and which MCP tools change its records, by operation (`pact.ContentDescriptor.Tools`); `Describe` and `Records` may report a record's first publication, ordered parents, authors and top heading level, and per locale its images, videos and heading outline. All of them are optional.
|
|
- Record events: `pact.RecordSaved` and `pact.RecordDeleted` are fired as pointers on the application's event bus after a record write commits, with the controller, the morph type, the id, the request locale and, for a save, the `pact.RecordChange` list of changed attributes with old and new values per locale. The admin framework fires them for every admin write and `cabana.EmitRecordSaved` lets plugin code that writes directly fire the same events.
|
|
- Singleton admin controllers: `pact.AdminSingleton` (`SingletonRecord`, first or create) makes a controller edit exactly one record, opened directly instead of from a list.
|
|
- A background job contract (`pact.Job`, `pact.JobArgs`) that does not depend on any queue library.
|
|
- A schedule contract: `pact.HasSchedule` returns `pact.ScheduledCommand` entries (a registered command name, its arguments and a `pact.Cadence` built with `pact.Daily`, `pact.DailyAt` or `pact.Every`), the Go form of WinterCMS `registerSchedule`. It does not depend on any queue library either.
|
|
- `pact.OptionalMessage`, a service an optional plugin can publish so others integrate with it without importing its package.
|
|
- `pact.HasModels` and `pact.OptionalMessage` are declared for plugins to implement, but no framework package consumes them yet.
|
|
|
|
## Usage
|
|
|
|
A plugin declares its capabilities by implementing the interfaces and asserting them at compile time:
|
|
|
|
```go
|
|
package blog
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
var (
|
|
_ pact.HasRoutes = (*Plugin)(nil)
|
|
_ pact.HasPermissions = (*Plugin)(nil)
|
|
)
|
|
|
|
type Plugin struct{}
|
|
|
|
func (p *Plugin) Routes(r pact.Router) error {
|
|
r.Group("/api/blog", []string{"auth"}, func(r pact.Router) {
|
|
r.Get("/posts", listPosts)
|
|
r.Get("/posts/{id}", showPost)
|
|
r.Where("id", "[0-9]+")
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) Permissions() []pact.Permission {
|
|
return []pact.Permission{
|
|
{Code: "acme.blog.access_posts", Tab: "Blog", Label: "Manage posts"},
|
|
}
|
|
}
|
|
|
|
func listPosts(w http.ResponseWriter, r *http.Request) {}
|
|
func showPost(w http.ResponseWriter, r *http.Request) {}
|
|
```
|
|
|
|
A plugin schedules one of its registered commands by implementing `pact.HasSchedule`:
|
|
|
|
```go
|
|
var _ pact.HasSchedule = (*Plugin)(nil)
|
|
|
|
func (p *Plugin) Schedule() []pact.ScheduledCommand {
|
|
return []pact.ScheduledCommand{
|
|
{Command: "blog:prune-drafts", Cadence: pact.Daily()},
|
|
{Command: "blog:ping", Args: []string{"--quiet"}, Cadence: pact.Every(15 * time.Minute)},
|
|
}
|
|
}
|
|
```
|
|
|
|
## API reference
|
|
|
|
| Identifier | Description |
|
|
|------------|-------------|
|
|
| `pact.Router` | Laravel-style route group builder (`pact.Router.Group`, `pact.Router.GroupRaw`, one method per HTTP verb, `pact.Router.Where`, `pact.Router.WhereIn`); implemented by surf. |
|
|
| `pact.HasRoutes` | Declares HTTP routes on a `pact.Router`. |
|
|
| `pact.Middleware` | A named `func(http.Handler) http.Handler` wrapper. |
|
|
| `pact.HasMiddleware` | Registers named middleware. |
|
|
| `pact.HasMiddlewareFactories` | Registers parameterized middleware resolved from `name:param` at wrap time. |
|
|
| `pact.HasHouseMiddleware` | Registers middleware tagged as envelope and error handling, which raw groups refuse. |
|
|
| `pact.HasConfig` | Ships default YAML config, merged under the plugin ID. |
|
|
| `pact.HasMigrations` | Ships an ordered gormigrate set, run with a per-plugin history table. |
|
|
| `pact.HasCommands` | Contributes bonfire console commands to the application binary. |
|
|
| `pact.HasModels` | Exposes GORM models. |
|
|
| `pact.Job` | Background unit of work that receives `pact.JobArgs`. |
|
|
| `pact.HasJobs` | Registers background jobs. |
|
|
| `pact.HasSchedule` | Declares recurring console commands (`Schedule() []pact.ScheduledCommand`); conga workers run them. |
|
|
| `pact.ScheduledCommand` | One schedule entry: `Command`, `Args` and `Cadence`. |
|
|
| `pact.Cadence` | Opaque run frequency with `IsZero`, `Interval` (24h for daily cadences) and `At` (hour and minute of a daily cadence). |
|
|
| `pact.Daily` | Cadence at 00:00 every day in the app timezone (Laravel `->daily()`). |
|
|
| `pact.DailyAt` | Cadence at a given hour and minute every day in the app timezone. |
|
|
| `pact.Every` | Cadence at every multiple of an interval since local midnight; the interval must be at least 1s and divide 24h. |
|
|
| `pact.HasLang` | Ships translation YAML under `lang/<locale>/<group>.yaml`. |
|
|
| `pact.HasLangOverrides` | Replaces or adds translations of any loaded namespace, including the framework's own. |
|
|
| `pact.HasMailTemplates` | Ships mail templates and layout aliases. |
|
|
| `pact.Permission` | One backend permission entry (code, tab, label, roles). |
|
|
| `pact.HasPermissions` | Declares backend permissions. |
|
|
| `pact.TokenScope` | One personal API token scope: name, label, description, permission patterns and HTTP methods. |
|
|
| `pact.HasTokenScopes` | Declares personal API token scopes compiled at admin boot. |
|
|
| `pact.FrontendTokenScope` | One member personal API token scope: `Name` (what a route's `user.scope:<name>` middleware checks), `Label`, `Description` and `Implies` (`""`, `read` or `write`; empty means not implied). The route middleware enforces the scope; `Implies` is data for a later OAuth MCP check and is not an expansion by itself. |
|
|
| `pact.HasFrontendTokenScopes` | Declares member token scopes, the frontend counterpart of `pact.HasTokenScopes`; compiled by the plugin that owns member tokens, never by the admin token catalog. |
|
|
| `pact.MemberTokenScopeCatalog` | The compiled member-token scope list, published on the application. `pact.MemberTokenScopeCatalog.MemberTokenScopes` returns `[]pact.FrontendTokenScope`. |
|
|
| `pact.ValidTokenScopeName` | Reports whether a scope name is valid (`^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)?$`); shared by both token catalogs. |
|
|
| `pact.NavigationItem` | One backend navigation entry, with an optional side menu and an optional `Visible` func the admin runs on every navigation request. |
|
|
| `pact.SettingsItem` | One settings screen entry: a singleton settings form (`Model`, `Form`, `NewModel`), or a link to an admin controller when `Controller` names its ID, as a WinterCMS `'url' => Backend::url(...)` entry. |
|
|
| `pact.AdminController` | Admin controller identity: ID, model name and YAML config directory. |
|
|
| `pact.AdminAssets` | Embedded tree of the plugin's admin YAML. |
|
|
| `pact.AdminRecordSource` | Supplies a new model record for the generic admin handlers. |
|
|
| `pact.AdminClientAssets` | Declares a controller's admin JS (`AdminJS`) and CSS (`AdminCSS`) files, paths under the plugin's `assets/` directory. |
|
|
| `pact.AdminAction` | One named controller action: name, label, extra permissions and the Go `Run` function. |
|
|
| `pact.AdminActionInput` | What an action receives: widget field, optional record id and scoped record, the fill snapshot, and the widget's raw JSON payload (`Payload`, at most 64 KiB, nil when absent, never set for a toolbar action, not inspected by the framework). |
|
|
| `pact.AdminActionResult` | What an action returns: a message for the toast, the fill write-back values, and an optional `Data` value passed through to the widget as `data` (any JSON up to 256 KiB encoded, not filtered like fill, omitted when nil). |
|
|
| `pact.HasAdminActions` | Registers a controller's actions for `toolbar.buttons` and `type: widget` fields. |
|
|
| `pact.AdminBulkAction` | One named bulk action: name, label, optional confirm text, extra permissions, the Go `Run` function and `Detached` (run outside the framework transaction, records loaded without row locks, for actions that call an external service). |
|
|
| `pact.AdminBulkActionInput` | What a bulk action receives: `Records`, the selected records loaded and row-locked through the list scope (without row locks for a `Detached` action). |
|
|
| `pact.AdminBulkActionResult` | What a bulk action returns: an optional message for the toast and `Affected`, the number of records it changed. |
|
|
| `pact.HasAdminBulkActions` | Registers a controller's bulk actions for the `bulkActions` list of `config_list.yaml`. |
|
|
| `pact.AdminRecordAction` | One named record action: name, label, optional confirm text, extra permissions, the `Danger` flag (the admin confirms it in the danger variant), an optional `Prompt` (the confirm dialog asks for one text value), the optional `Applies` rule and the Go `Run` function. |
|
|
| `pact.AdminActionPrompt` | The text input of a prompted record action: `Label` (required), `Placeholder`, `Comment` (phrase keys or text) and `MaxLength` in runes (0 means 255). |
|
|
| `pact.AdminRecordActionInput` | What a record action receives: `RecordID` and `Record`, the record loaded and row-locked through the form scope, and `Value`, the trimmed prompt answer (empty without a `Prompt`). |
|
|
| `pact.AdminRecordActionResult` | What a record action returns: an optional message for the toast. |
|
|
| `pact.HasAdminRecordActions` | Registers a controller's record actions for the `recordActions` list of `config_form.yaml`. |
|
|
| `pact.AdminActionExtension` | What a plugin adds to one admin controller by its `Controller` ID: `RecordActions`, offered on the target's preview screen after the owner's actions, each checked against the target's `RequiredPermissions` and its own `Permissions` (a contribution to `summercms.cabana.users` is refused with `user.outranked` when the target administrator outranks the caller), and `Form`, an optional `pact.AdminFormExtension`. |
|
|
| `pact.HasAdminActionExtensions` | Implemented by a plugin (not a controller) that adds actions or form fields to admin controllers, its own or another plugin's. An unknown controller ID, a target without a preview block (record actions) or a form (form fields), or a duplicate name stops the boot. |
|
|
| `pact.AdminFormExtension` | Fields and tabs a plugin places in another controller's form, loaded and saved by the plugin itself: a fields document in the `fields.yaml` format (`Fields`, a path in the plugin's `AdminFS`, or `FieldsYAML`, the bytes built at boot) whose fields take `before:`/`after:` anchors and `zone:` and whose `tabs:` list places new tabs; `Hide` (owner fields to leave out), `Permissions` and `Visible` (who sees the fields), `Dir`, `PartialData` and `Actions` (its partial and widget fields), `Load` (values on show) and `Save` (values in the record's save transaction). The owner's model is never written. |
|
|
| `pact.AdminFormLoad` | What `Load` receives: the read or save transaction `DB`, the target `Controller` ID, the loaded `Record` and its `ID` as text. |
|
|
| `pact.AdminFormSave` | What `Save` receives: the save transaction `DB`, `Controller`, the written `Record`, its `ID`, `Created` (true on create), `Values`, the submitted values of the extension's fields (a translatable field as a `map[string]string` of locale to text), and `Changes`, what the owner's save changed (as `pact.RecordSaved` will report it). |
|
|
| `pact.AdminFormSaveResult` | What `Save` returns: `Notices`, save notices per field name (`pact.FormFieldNotice`), shown like a field type's notices. |
|
|
| `pact.AdminFormErrors` | A `Save` refusal: field name to messages (phrase keys or text). The save answers 422 with them on the fields and rolls back. |
|
|
| `pact.AdminFormZoneTabs` | The `zone:` value of the form's tab zone, the default and the only zone rendered so far; `pact.AdminFormZoneOutside` and `pact.AdminFormZoneSecondary` are reserved and stop the boot ("is not supported yet"). |
|
|
| `pact.HasAccountContributions` | Implemented by a plugin that adds sections to the admin's My account page. |
|
|
| `pact.AccountContribution` | One My account section: `Name` (an identifier unique across plugins), `Label`, `Description`, an optional pure-read `Status` func for the signed-in administrator and its `Actions`. A non-identifier or taken name, an empty label, a duplicate or non-identifier action name, a nil `Run` or an empty action label stops the boot. |
|
|
| `pact.AccountAction` | One button of a section: name, label, optional `Confirm` text, `Danger`, extra `Permissions`, an optional pure-read `Applies` rule and the Go `Run` function, which acts for the signed-in administrator only. |
|
|
| `pact.AccountActionInput` | What an account action receives: `AdminID`, the signed-in administrator, and nothing else. |
|
|
| `pact.AccountActionResult` | What an account action returns: a `Message` for the toast and an optional `URL` the admin opens in a new tab. |
|
|
| `pact.HasMCP` | Implemented by a plugin that declares MCP tools and prompts for the framework's MCP endpoint. |
|
|
| `pact.MCP` | The declaration: `Tools` and `Prompts` (resources are a later field of the same struct). |
|
|
| `pact.MCPTool` | One tool bound to one of the plugin's own token-guarded routes by `Method` and `Path`: unprefixed `Name`, static `Title` and `Description`, the `Guard` it runs as, the `Input` struct (zero value; `path`, `query`, `json` and `jsonschema` tags map and describe the arguments), an optional `Output` struct, extra `Permissions` for the listing and the `ReadOnly`, `Destructive`, `Idempotent` and `OpenWorld` overrides of the annotations derived from the method (nil means derived). |
|
|
| `pact.MCPPrompt` | One prompt template: name, title, description, guard, permissions, `Arguments` (`pact.MCPPromptArgument`), the `Render` func returning `pact.MCPPromptMessage` values (`Role` user or assistant, `Text`) and `Writes`, set when the prompt directs tools that are not read-only so it is listed only for an identity that may write (zero value: offered to read-only grants too). |
|
|
| `pact.MCPGuard` | A guard provider the owner of a route guard publishes: its name, whether a middleware list is token-guarded by it, the scopes and permissions a middleware list requires (`pact.MCPRequirements`), whether a subject may call a tool, and the pre-resolved principal for one in-process call (`pact.MCPDispatch`). |
|
|
| `pact.MCPGuards` | The registry of guard providers published on the application; `pact.NewMCPGuards` creates it, `Register` refuses an empty or duplicate name with a `pact:` error naming the guard, `Lookup` finds one and `Names` lists them sorted. Safe for concurrent use. |
|
|
| `pact.MCPSubject` | The identity an MCP session acts as under one guard: guard name, user id, granted scopes, OAuth client id and token id. `pact.MCPSubject.TokenID` is a member personal-token id only when `pact.MCPSubject.ClientID` is empty, and the OAuth access-token row otherwise. `pact.MCPSubject.Scopes` is the grant, not an implied expansion. |
|
|
| `pact.MCPCall` | One in-process MCP tool call: `pact.MCPCall.Subject`, `pact.MCPCall.SessionID` (empty on the stateless path) and `pact.MCPCall.Tool`. |
|
|
| `pact.WithMCPCall` | Stores a `pact.MCPCall` on a context. A nil context is returned as nil. |
|
|
| `pact.MCPCallFrom` | Returns the `pact.MCPCall` on a context. The boolean is false when the context is nil or carries none, including a plain HTTP request. |
|
|
| `pact.MCPDispatch` | A guard provider's answer for one call: the principal (as `any`), an optional credential and the bouncer guard names that honour the dispatch marker. |
|
|
| `pact.MCPTokenVerifier` | Published by the OAuth server plugin: verifies an access token at the MCP endpoint into a `pact.MCPGrant` (user, client, token, scopes, expiry and, for a `backend` grant, `BackendUserID`, the backend account the consent showed) and names its resource. A refused token is `pact.ErrMCPTokenInvalid`. The interface is only `MCPVerifyToken` and `MCPResource`. |
|
|
| `pact.MCPAdvertisedScopes` | Optional second interface on that verifier. `pact.MCPAdvertisedScopes.MCPScopesSupported` is the MCP scope list pier copies into protected-resource metadata when the slice is non-empty. |
|
|
| `pact.MCPPersonalTokenResolver` | Published by the one plugin whose personal tokens may open an MCP session (one resolver per application): resolves a raw token into a `pact.MCPSubject` whose `TokenID` identifies the token row (it keys the session binding; a subject without one is refused); false when the prefix is not its own, which the endpoint answers as unauthorized. |
|
|
| `pact.MCPLinkedBackendResolver` | Published by the plugin that owns the frontend-to-backend account link: the `pact.LinkedBackendUser` (id, login, display name) of a frontend user, false when unlinked. |
|
|
| `pact.MCPSubjectChanged` | Event fired on the festival bus when a subject's permissions, link, grants or existence change (`Kind` is `pact.MCPSubjectPermissions`, `pact.MCPSubjectLink`, `pact.MCPSubjectRevoked` or `pact.MCPSubjectDeleted`), so the MCP endpoint re-lists tools or ends sessions. `TokenID` and `ClientID` narrow a revoked event: a token id ends only the sessions that personal token opened (subject with that `TokenID` and no `ClientID`), a client id only that OAuth client's sessions, neither every session of the subject; other kinds ignore them. |
|
|
| `pact.FrontendPermission` | One frontend permission code a plugin declares: `Code`, `Label`, `Tab` and `Comment`, offered in the user plugin's permission editors next to the seeded codes. |
|
|
| `pact.HasFrontendPermissions` | Declares frontend permission codes, the frontend counterpart of `pact.HasPermissions`. |
|
|
| `pact.ReorderAfterSave` | Optional controller hook `ReorderAfterSave(ctx, partition)`: runs after a reorder save wrote the new order, inside the same transaction (`cabana.TxFromContext` works); an error rolls the reorder back. |
|
|
| `pact.AdminPartialData` | Supplies the view model a controller partial template renders; never the GORM model. |
|
|
| `pact.HasFormFieldTypes` | Implemented by a plugin (not a controller) that registers admin form field types. |
|
|
| `pact.FormFieldType` | One field type: the alias `Name`, the custom-element tag `Element`, `Translatable`, the `Scripts` and `Styles` under `assets/`, the `Options` keys, and the funcs `CheckOptions` (optional), `Decode` (required), `Load`, `ListValue` (`func(ctx context.Context, value any) string`), `Upload` and `RequestOptions` (`func(ctx context.Context, db *gorm.DB) (map[string]any, error)`, merged under the fields.yaml options of each form schema request) (optional). |
|
|
| `pact.FormFieldValue` | What `Decode` and `Load` receive: the transaction, the record, the field, its options, the content locale (translatable types), whether the value lives in the host column, the value (always a string for a translatable type) and an exact reader of another attribute in the same locale. |
|
|
| `pact.FormFieldDecoded` | `Decode`'s answer: the `Value` to store (a string for a translatable type) and optional `Notices`. |
|
|
| `pact.FormFieldNotice` | A message about a saved value: a phrase key or text with params; a positive `Count` selects the plural form. |
|
|
| `pact.FormFieldUpload` | What `Upload` receives: the transaction, the scoped record (nil on the create form), the field, its options, the file name and the capped body. |
|
|
| `pact.FormFieldUploadResult` | `Upload`'s answer: the URL of the stored file. |
|
|
| `pact.FormFieldError` | A value or upload a field type refuses: messages shown on the field (validation failed). |
|
|
| `pact.ListRowStates` | Optional controller hook called once per list page; returns the states of the page's records, index-aligned. |
|
|
| `pact.ListExtendRecords` | Optional controller hook called once per list page, before the columns are read; fills unstored fields of the page's records. |
|
|
| `pact.FormRecordTitle` | Optional controller hook naming a record on its form and preview screens; sent as the record envelope's `meta.title`. |
|
|
| `pact.RowState` | One state of a list row: `pact.RowStateDeleted`, `pact.RowStateNegative` or `pact.RowStateDisabled`. |
|
|
| `pact.FormVirtualFields` | Optional controller list of form fields that are not model columns for the form (a password and its confirmation, for example): never bound, filled or returned; their submitted values reach the Form hooks through the admin framework's context accessor. |
|
|
| `pact.FormRules` | Optional controller hook returning the validation rules of an admin save for `create` or `update`; the set replaces the model's `Rules()` for those saves and may name virtual fields. |
|
|
| `pact.FilterScope` | Model scopes a list filter may call, limited to an exact allow list. |
|
|
| `pact.FilterOptions` | Serves the choices of a scope filter. The admin controller may implement it and is asked first, so choices can be read from the database; otherwise the model is asked. |
|
|
| `pact.RelationBeforeLink` | Optional controller hook that checks or fills pivot columns before a relation link is written. |
|
|
| `pact.RelationBeforeCreate` | Optional controller hook run in the write transaction before a relation manager creates a related record. |
|
|
| `pact.RelationAfterCreate` | Optional controller hook run after a relation manager creates a related record, before the commit. |
|
|
| `pact.RelationBeforeUpdate` | Optional controller hook run in the write transaction before a relation manager updates a related record. |
|
|
| `pact.RelationAfterUpdate` | Optional controller hook run after a relation manager updates a related record, before the commit. |
|
|
| `pact.RelationBeforeDelete` | Optional controller hook run in the write transaction before a relation manager deletes a related record. |
|
|
| `pact.RelationAfterDelete` | Optional controller hook run after a relation manager deletes a related record, before the commit. |
|
|
| `pact.Option` | One dropdown choice (value and label), with `Depth`, the nesting level of a choice taken from a tree (omitted from JSON when 0). |
|
|
| `pact.AdminSingleton` | Optional controller capability: `SingletonRecord` returns the one record the controller edits, creating it when absent; the admin opens it directly and the list, create, bulk and delete routes answer 404. |
|
|
| `pact.DependentOptionsProvider` | Optional controller hook serving the choices of a dropdown with `dependsOn` from the submitted values of its siblings; asked by the options route and on every save. |
|
|
| `pact.DependentOptionSet` | `DependentOptions`' answer: `Applies` (false when the sibling's value takes no choice; the field then stores null) and `Options`. |
|
|
| `pact.HasMenuItemTypes` | Implemented by plugins that register menu item types; collected once at boot by signpost. |
|
|
| `pact.MenuItemType` | One menu item type: `Key`, `Label`, `DefaultPattern` (required, starts with `/`), `PatternFrom` (a list type's record type), `Placeholders`, `Nesting`, `DynamicItems`, the optional `References` func, the required `Resolve` func and the optional `Content` descriptor. |
|
|
| `pact.MenuItemReference` | One record a menu item can point at: value, label and nested records. |
|
|
| `pact.MenuItemResolve` | What `Resolve` receives: type, reference, the layered URL pattern, whether to expand nested items and the unprefixed locale. |
|
|
| `pact.ResolvedMenuItem` | A resolved item: title, URL (empty for a pure list type), modification time, ordered alternates and nested items. |
|
|
| `pact.MenuItemAlternate` | The URL and title of a resolved item in one locale. |
|
|
| `pact.ContentDescriptor` | The records behind a menu item type: `Controller` (the admin controller ID that edits them), `Model` (their morph type, required), `Describe` (required), the optional `Records` and `Match`, `Kind` (one of the kind constants below, "" when unsaid) and `Tools` (operation to full MCP tool name). The funcs read through `cabana.TxFromContext` when the context carries a write transaction. |
|
|
| `pact.ContentKindArticle`, `pact.ContentKindCollection`, `pact.ContentKindProfile`, `pact.ContentKindPage` | The values of `ContentDescriptor.Kind`: a dated piece with authors, a record that lists others, a person or organisation, a standalone page. |
|
|
| `pact.ContentToolEdit`, `pact.ContentToolHeadings`, `pact.ContentToolImageAlt` | The keys of `ContentDescriptor.Tools`: edit a record, change its body's headings, set its images' alternative text. |
|
|
| `pact.ContentList` | What `Records` receives: the layered pattern, `After` (the last ID of the previous page), `Limit` and the unprefixed locale. |
|
|
| `pact.ContentPage` | One page of `Records`, and `Next`, the `After` of the following page ("" on the last). |
|
|
| `pact.ContentDescribe` | What `Describe` receives: the record `ID`, the layered pattern and the unprefixed locale. |
|
|
| `pact.ContentRecord` | A described record: `ID`, `Published`, `LastMod` and its `Locales`, and optionally `PublishedAt` (first publication), `Parents` (ordered, the first is the default primary), `Authors` and `TopHeading` (the highest heading level the body may use, 0 for no rule). |
|
|
| `pact.ContentLocale` | A record in one locale: `URL` ("" without a public page), `Title`, `Description`, `Image` and `Words`, and optionally `Images`, `Videos` and `Headings`. |
|
|
| `pact.ContentRef` | Another record: a menu item type key (`Type`) and the record `ID`. |
|
|
| `pact.ContentPerson` | A person named on a record: `Name` and a `Ref` to their own record (`Type` "" when they have none). |
|
|
| `pact.ContentImage` | An image a record shows: `URL`, `Alt`, `Decorative` (no content, empty `Alt` intended), `Width` and `Height` (0 when unknown). |
|
|
| `pact.ContentVideo` | A video a record embeds: `Provider` and the video `ID` there. |
|
|
| `pact.ContentHeading` | One heading of a record's body: `Level` (1 to 6) and `Text`. |
|
|
| `pact.ContentMatch` | What `Match` receives: a public `Path`, the layered pattern and the unprefixed locale. |
|
|
| `pact.ContentMatchResult` | `Match`'s answer: the record `ID`, the `Locale` the path addresses and `Exact` (false when a segment does not match the record). |
|
|
| `pact.RecordSaved` | Event fired as a pointer after a record create or update commits: `Controller` ("" outside the admin forms), `Model` (morph type), `ID`, `Locale`, `Created` and `Changes`. |
|
|
| `pact.RecordDeleted` | Event fired as a pointer after a record delete commits, one per record: `Controller`, `Model`, `ID`. |
|
|
| `pact.RecordChange` | One changed attribute: `Attribute`, `Locale` ("" for the record's own row, the locale of a translated value), `Old` (nil on create) and `New`. |
|
|
|
|
## Dependencies
|
|
|
|
- SummerCMS modules: [bonfire](../bonfire/README.md) (the command type in `pact.HasCommands`).
|
|
- Third-party: `github.com/go-gormigrate/gormigrate/v2`, `gorm.io/gorm`.
|
|
- Standard library: `context`, `encoding/json`, `io`, `io/fs`, `net/http`, `regexp`, `strings`, `time`.
|
|
|
|
## Testing
|
|
|
|
```sh
|
|
go test ./modules/pact/...
|
|
```
|
|
|
|
The tests are compile-time interface checks and need no external services.
|