pluginsdk/plugin/deps.go
Alex Dunmow 6dd745bc34 Let a plugin grant and revoke file access
WO-FL-009. The files family reaches the CMS File access grant store: a plugin
grants one file to a member or an address, lists what a file has handed out,
and revokes what it gave. The response carries the signed unlock link, so a
seller can deliver a file to somebody with no account. Nothing on the wire
names the calling plugin: the host stamps plugin:<name> on what this family
writes and refuses a revoke of anything else. ADR 0008.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 10:47:13 +08:00

200 lines
7.2 KiB
Go

package plugin
import (
"context"
"net/http"
"time"
"connectrpc.com/connect"
"git.dev.alexdunmow.com/block/pluginsdk/ai"
"git.dev.alexdunmow.com/block/pluginsdk/auth"
"git.dev.alexdunmow.com/block/pluginsdk/content"
"git.dev.alexdunmow.com/block/pluginsdk/crypto"
"git.dev.alexdunmow.com/block/pluginsdk/datasources"
"git.dev.alexdunmow.com/block/pluginsdk/datatables"
"git.dev.alexdunmow.com/block/pluginsdk/gating"
"git.dev.alexdunmow.com/block/pluginsdk/menus"
"git.dev.alexdunmow.com/block/pluginsdk/settings"
"git.dev.alexdunmow.com/block/pluginsdk/subscriptions"
"github.com/google/uuid"
)
// CoreServices provides CMS capabilities to plugins.
type CoreServices struct {
// Capability interfaces — typed access to CMS functionality
Content content.Content
ContentAuthor content.Author
Settings settings.Settings
Gating gating.Gating
Crypto crypto.Crypto
Menus menus.Menus
Datasources datasources.Datasources
DataTables datatables.DataTables
PublicUsers auth.PublicUsers
Subscriptions subscriptions.Subscriptions
// Database — for plugin's own sqlc queries
Pool Pool
// RPC interceptors — core-provided Connect handler options
Interceptors connect.Option
// Site configuration
MediaPath string
AppURL string
// Media library — deposit images through the full upload pipeline
Media Media
// AI
ToolRegistry ai.ToolRegistry
AITextCall func(ctx context.Context, taskKey, systemPrompt, userMessage string) (string, error)
// Email
EmailSender EmailSender
// Plugin interop
Bridge PluginBridge
// OutboundHTTP is the host-mediated egress transport (ADR 0023, cms repo).
// Plugins make outbound requests through &http.Client{Transport:
// deps.OutboundHTTP}; the host enforces the plugin's allowed_hosts grant,
// the SSRF guard, and the platform denylist. Nil when the plugin declared
// no allowed_hosts — a request through it fails closed.
OutboundHTTP http.RoundTripper
// Core RPC services — pre-built bindings for CMS-provided services
CoreServiceBindings CoreServiceBindings
ReviewSubmitter ReviewSubmitter
BadgeRefresher BadgeRefresher
SettingsUpdater settings.Updater
FileGrants FileGrants
// Extension points — typed as narrow interfaces where possible
JobRunner JobRunner
EmbeddingService EmbeddingService
RAGService RAGService
// Provisioner — idempotent ensure/seed operations. In the wasm world this
// is a LOAD-TIME capability: call it from the Load hook, not from
// Register/RegisterWithProvisioner (DESCRIBE stubs every host function to
// fail, so register-time provisioning cannot cross the ABI).
Provisioner Provisioner
}
// MediaDeposit describes an image a plugin deposits into the CMS media library.
// The bytes flow through the same pipeline as an admin upload (optimize, WebP,
// thumbnails, LQIP, dimensions); responsive variants are generated on demand.
type MediaDeposit struct {
// ID is the media row's primary key. REQUIRED for Provisioner.EnsureMedia —
// it is the deterministic, template-referable key a seeded {% img %} tag
// resolves by. Optional for Media.Deposit, where a zero value is generated.
ID uuid.UUID
// Filename is the original filename, e.g. "hero.jpg".
Filename string
// Data is the raw image bytes, typically from go:embed.
Data []byte
// AltText is optional accessibility text.
AltText string
// Folder optionally groups the media under a named library folder,
// created if absent.
Folder string
// Source is an optional provenance label, e.g. the plugin name.
Source string
}
// MediaResult reports the outcome of a deposit.
type MediaResult struct {
// ID is the media row's primary key (the supplied ID, or a generated one).
ID uuid.UUID
// Ref is a ready-to-use reference ("media:<uuid>") for a block src or a
// {% img %} tag.
Ref string
// Created is false when an existing row with the supplied ID was reused.
Created bool
}
// Media lets a plugin deposit images into the CMS media library at runtime.
// Seed-time deposits use Provisioner.EnsureMedia instead — it is idempotent
// (skip if the ID exists; on changed bytes it warns rather than overwriting).
type Media interface {
Deposit(ctx context.Context, deposit MediaDeposit) (MediaResult, error)
}
// JobRunner submits background jobs for async processing.
type JobRunner interface {
Submit(ctx context.Context, jobType string, config []byte) error
}
// EmbeddingService generates and manages text embeddings.
type EmbeddingService interface {
GenerateEmbedding(ctx context.Context, text string) ([]float32, error)
EmbedContent(ctx context.Context, sourceType string, sourceID uuid.UUID, text string) (bool, error)
IsAvailable() bool
}
// ContentFetcher retrieves the title and full text for a content item so the
// RAG service can re-index it after changes. Plugins register one per content type.
type ContentFetcher func(ctx context.Context, contentID uuid.UUID) (title string, text string, err error)
// RAGService provides retrieval-augmented generation for AI agents.
type RAGService interface {
Query(ctx context.Context, query string, limit int) ([]RAGResult, error)
RegisterContentFetcher(contentType string, fetcher ContentFetcher)
OnContentChanged(ctx context.Context, contentType string, contentID uuid.UUID)
}
// RAGResult is a single result from a RAG query.
type RAGResult struct {
Content string
Score float64
Metadata map[string]string
}
// FileGrants hands one File library file to one person and takes it back. A
// grant is "this person may download this file whatever tier they hold", which
// is how a storefront plugin delivers a purchase and withdraws it on a refund.
// The host attributes every grant written here to the calling plugin, and a
// plugin may revoke only the grants it wrote.
type FileGrants interface {
GrantFileAccess(ctx context.Context, params FileGrantParams) (FileGrant, error)
RevokeFileAccess(ctx context.Context, grantID uuid.UUID) error
ListFileGrants(ctx context.Context, attachmentKind string, attachmentID uuid.UUID) ([]FileGrant, error)
}
// FileGrantParams names the file and exactly one subject: a public user, or an
// email address that receives a signed unlock link instead.
type FileGrantParams struct {
// AttachmentKind is "library" or "table"; empty means "library".
AttachmentKind string
AttachmentID uuid.UUID
PublicUserID uuid.UUID
Email string
// ExpiresAt zero means the grant ends only when it is revoked.
ExpiresAt time.Time
}
// FileGrant is one stored grant. A revoked grant is kept as the record of who
// was let in and when that stopped.
type FileGrant struct {
GrantID uuid.UUID
AttachmentKind string
AttachmentID uuid.UUID
PublicUserID uuid.UUID
Email string
Source string
ExpiresAt time.Time
RevokedAt time.Time
CreatedAt time.Time
// UnlockURL opens the file for this grant. It is empty when the file has no
// permanent link to sign.
UnlockURL string
}
// BadgeRefresher recomputes badges for a data table row.
// The CMS handles loading the table schema, aggregating ratings,
// evaluating badge rules, and persisting the updated badge list.
type BadgeRefresher interface {
RefreshBadges(ctx context.Context, tableID, rowID uuid.UUID) error
}