agent

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ContentHandler

type ContentHandler struct {
	// contains filtered or unexported fields
}

ContentHandler exposes the Bearer-authenticated agent content endpoints. It reuses the existing ContentService (which already validates custom fields and fires plugin hooks) — it never duplicates that logic.

func NewContentHandler

func NewContentHandler(s ContentService, logger *util.Logger) *ContentHandler

NewContentHandler constructs a ContentHandler backed by the given content service. A nil logger degrades to a discard sink so the handler is safe to construct in any context (mirrors NewAPIKeyHandler/NewAPIKeyAuthMiddleware).

func (*ContentHandler) Create

func (h *ContentHandler) Create(w http.ResponseWriter, r *http.Request)

Create handles POST /api/v1/content. It maps the streamlined agent payload onto a contentdomain.CreateContentRequest (reusing the same validation + plugin hook path as the admin surface) and returns the created content in the envelope.

func (*ContentHandler) Delete

func (h *ContentHandler) Delete(w http.ResponseWriter, r *http.Request)

Delete handles DELETE /api/v1/content/{id}. It pre-fetches for the ownership check (404 on not-found or not-owned-per-role) then delegates to the existing ContentService.DeleteContent. A successful delete returns 204 No Content; a subsequent GET returns 404 (the service hard-deletes for Admins and scoped-deletes otherwise).

func (*ContentHandler) Get

Get handles GET /api/v1/content/{id}. It returns the content in the envelope, or 404 NOT_FOUND when it does not exist OR the caller is not allowed to see it.

Visibility scoping (review fix for the GET IDOR): the underlying GetByID returns content regardless of owner/status, so the handler enforces "published OR owned by the requesting user" and otherwise returns NOT_FOUND (never FORBIDDEN, so existence is not disclosed to unauthorized callers). This mirrors the ownership pattern the admin Update/DeleteContent paths use (existing.UserID != userID).

func (*ContentHandler) List

List handles GET /api/v1/content. It returns the caller's own content (any status — drafts + published) in newest-first order using opaque keyset (cursor) pagination, each item projected via the ContentProjection whitelist. hasMore/nextCursor are computed by requesting limit+1 rows; nextCursor is the opaque token of the last item on the current page and is only present when there is another page.

Optional filters: ?tag=foo&tag=bar (AND-of-tags), ?language=en, ?status=draft|published, ?post_type=post, ?author=alice (admin only — non-admins get 403), ?search=foo (min 2 chars). All filters AND with the cursor pagination. The agent v1 surface scopes to the caller's own content; the author filter is meaningful only for admins because the agent handler never expands the userID scope (admin still sees their own content here — use the admin surface for cross-user listings).

func (*ContentHandler) Publish

func (h *ContentHandler) Publish(w http.ResponseWriter, r *http.Request)

Publish handles POST /api/v1/content/{id}/publish. It delegates to ContentService.Publish so ownership, the draft→published transition, SEO auto-generation, and the AfterPublish hook fire through the same domain path the Update endpoint uses. The endpoint accepts an empty body and is idempotent: publishing an already-published post returns 200 with the current projection, fires no hook, runs no SEO regen.

A non-admin caller must be the owner; otherwise 404 (never 403, so existence is not disclosed). An admin can publish another user's content. Validation and persistence errors map to the same error envelope the other endpoints use.

func (*ContentHandler) Unpublish

func (h *ContentHandler) Unpublish(w http.ResponseWriter, r *http.Request)

Unpublish handles POST /api/v1/content/{id}/unpublish. It delegates to ContentService.Unpublish so the status flip persists through the same domain path. The endpoint accepts an empty body and is idempotent: unpublishing an already-draft post returns 200 with the current projection, fires no hook, runs no SEO regen. Unpublish never fires the AfterPublish hook (the hook is wired to the draft→published edge only).

Same ownership/role contract as Publish: 404 (not 403) for non-admin non-owners; admins can unpublish anyone's content.

func (*ContentHandler) Update

func (h *ContentHandler) Update(w http.ResponseWriter, r *http.Request)

Update handles PUT /api/v1/content/{id}. It pre-fetches the existing content for an ownership check (404 NOT_FOUND when the caller is neither owner nor Admin — existence is never disclosed) and then delegates to the existing ContentService.Update so hooks + custom-field validation run through the same domain path the admin uses. format=tiptap stores the body unchanged; markdown is rejected (Story 2.4).

v1 settable on update: title, body, format, postType, customFields, isPublished, tags, language. Server-managed (preserved from the existing item): SEO metadata (metaDescription, ogTitle, ogDescription), allowComments, translationGroupId.

type ContentProjection

type ContentProjection struct {
	ID           int            `json:"id"`
	Title        string         `json:"title"`
	Slug         string         `json:"slug"`
	Body         string         `json:"body"`
	Status       string         `json:"status"`
	PostType     string         `json:"postType,omitempty"`
	Language     string         `json:"language,omitempty"`
	Tags         []string       `json:"tags,omitempty"`
	CustomFields map[string]any `json:"customFields,omitempty"`
	Author       string         `json:"author,omitempty"`
	CreatedAt    string         `json:"createdAt,omitempty"`
	UpdatedAt    string         `json:"updatedAt,omitempty"`
}

ContentProjection is the public, whitelisted view of a content item returned by the agent create/get endpoints. It deliberately excludes internal/admin-only fields the raw contentdomain.Content carries (the numeric owner id, updatedBy/updatedByUsername, translationGroupId, SEO metadata, allowComments) so the programmatic API surface does not over-disclose to integrators or — via IDOR — to other tenants.

type ContentRequest

type ContentRequest struct {
	Title        string         `json:"title"`
	Body         string         `json:"body"`
	Format       string         `json:"format,omitempty"`
	PostType     string         `json:"postType,omitempty"`
	Slug         string         `json:"slug,omitempty"`
	CustomFields map[string]any `json:"customFields,omitempty"`
	IsPublished  bool           `json:"isPublished,omitempty"`
	Tags         []string       `json:"tags,omitempty"`
	Language     string         `json:"language,omitempty"`
}

ContentRequest is the agent authoring payload for POST /api/v1/content. It maps onto the existing contentdomain.CreateContentRequest (see the mapping table in the story Dev Notes). It is intentionally separate from the admin handler's CreateContentRequest so the agent contract can evolve independently.

Field notes:

  • Body is the canonical content. For Format "tiptap" (default) it is the Tiptap JSON string, stored unchanged. The service validates/sanitizes it.
  • Format defaults to "tiptap" when omitted. "markdown" is reserved for Story 2.4 (Markdown→Tiptap conversion) and is rejected with VALIDATION_ERROR here.
  • Slug is accepted for API stability but NOT honored in Story 2.1: the content service auto-generates the slug from the title. See Completion Notes.
  • IsPublished true maps to StatusPublished; false/omitted maps to StatusDraft.
  • Tags are normalized via contentdomain.ValidateTags; an invalid tag returns VALIDATION_ERROR from the server.
  • Language must be in the server's configured languages (config.toml [languages] list); an unknown code returns VALIDATION_ERROR (ErrInvalidLanguage). The CLI cannot pre-validate this and forwards the value as-given so the server is the single source of truth.

type ContentResponse

type ContentResponse struct {
	Content ContentProjection `json:"content"`
}

ContentResponse wraps a projected content item for the agent create/get responses. Envelope body: {"data":{"content":{...}}}.

func NewContentResponse

func NewContentResponse(c *contentdomain.Content) ContentResponse

NewContentResponse builds a ContentResponse projecting the given content entity to its public fields. A nil entity yields an empty projection (defensive — the service contract returns a non-nil content on success).

type ContentService

type ContentService interface {
	Create(ctx context.Context, userID int, req contentdomain.CreateContentRequest) (*contentdomain.Content, error)
	GetByID(ctx context.Context, id int) (*contentdomain.Content, error)
	ListByCursor(ctx context.Context, userID int, limit int, beforeID int, filters contentdomain.ContentFilters) ([]*contentdomain.Content, error)
	Update(ctx context.Context, id int, userID int, role string, req contentdomain.UpdateContentRequest) (*contentdomain.Content, error)
	DeleteContent(ctx context.Context, id int, userID int, role string) error
	Publish(ctx context.Context, id int, userID int, role string) (*contentdomain.Content, error)
	Unpublish(ctx context.Context, id int, userID int, role string) (*contentdomain.Content, error)
}

ContentService is the narrow slice of the content domain service the agent handlers depend on. *contentdomain.Service satisfies it. Declaring it locally — instead of depending on the admin handler's wider ContentServiceInterface — keeps the agent surface independent and makes the dependency explicit. It is exported so mockery can generate an exported constructor callable from the package's *_test files (CLAUDE.md mandates *_test packages, which cannot reach an unexported mock).

type MediaHandler

type MediaHandler struct {
	// contains filtered or unexported fields
}

MediaHandler exposes the Bearer-authenticated agent media endpoints. It reuses the existing MediaService (which already validates files, hashes, dedups, converts to WebP, and builds thumbnail variants) — it never duplicates that logic.

func NewMediaHandler

func NewMediaHandler(s MediaService, logger *util.Logger) *MediaHandler

NewMediaHandler constructs a MediaHandler backed by the given media service. A nil logger degrades to a discard sink so the handler is safe to construct in any context (mirrors NewContentHandler).

func (*MediaHandler) Get

Get handles GET /api/v1/media/{id}. It returns the media in the envelope, or 404 NOT_FOUND when it does not exist OR the caller is not allowed to see it (ownership scoping: only the owner or an Admin may read a given item — existence is never disclosed, consistent with the content visibility model).

func (*MediaHandler) List

func (h *MediaHandler) List(w http.ResponseWriter, r *http.Request)

List handles GET /api/v1/media. It returns the caller's own media in newest-first order using opaque keyset (cursor) pagination — the SAME contract as the content list. It reuses the agent package's cursor helpers and the response package's SuccessList/ Pagination/ListMeta types (introduced for this reuse in Story 2.2).

func (*MediaHandler) Upload

func (h *MediaHandler) Upload(w http.ResponseWriter, r *http.Request)

Upload handles POST /api/v1/media. It parses a required `file` part and an optional JSON `metadata` part (carrying altText) from multipart/form-data, maps them onto a mediadomain.UploadRequest, and delegates to the existing MediaService.Upload. A missing `file` part returns 400 VALIDATION_ERROR; the service's own validation (mime/size/alt text) and dedup flow through handleMediaError.

type MediaMetadata

type MediaMetadata struct {
	AltText string `json:"altText,omitempty"`
}

MediaMetadata is the optional JSON `metadata` part of a media upload. altText is the only field today; it is REQUIRED by the media service (ValidateAltText) for accessibility, so agents should always send it.

type MediaProjection

type MediaProjection struct {
	ID               int                                 `json:"id"`
	Filename         string                              `json:"filename"`
	OriginalFilename string                              `json:"originalFilename"`
	MimeType         mediadomain.MimeType                `json:"mimeType"`
	FileSize         int64                               `json:"fileSize"`
	Width            int                                 `json:"width"`
	Height           int                                 `json:"height"`
	AltText          string                              `json:"altText"`
	IsWebP           bool                                `json:"isWebp"`
	Hash             string                              `json:"hash"`
	URL              string                              `json:"url"`
	Variants         map[string]mediadomain.MediaVariant `json:"variants,omitempty"`
	CreatedAt        string                              `json:"createdAt,omitempty"`
	UpdatedAt        string                              `json:"updatedAt,omitempty"`
}

MediaProjection is the public, whitelisted view of a media item returned by the agent media endpoints. It deliberately excludes internal/admin-only fields the raw mediadomain.Media carries (the numeric owner id, filePath, uploadedBy) so the programmatic API surface does not over-disclose — mirroring ContentProjection.

type MediaResponse

type MediaResponse struct {
	Media MediaProjection `json:"media"`
}

MediaResponse wraps a projected media item for the agent media responses. Envelope body: {"data":{"media":{...}}}.

func NewMediaResponse

func NewMediaResponse(m *mediadomain.Media) MediaResponse

NewMediaResponse builds a MediaResponse projecting the given media entity to its public fields. A nil entity yields an empty projection (defensive — the service contract returns a non-nil media on success).

type MediaService

type MediaService interface {
	Upload(ctx context.Context, req mediadomain.UploadRequest) (*mediadomain.Media, error)
	GetByID(ctx context.Context, id int) (*mediadomain.Media, error)
	ListByCursor(ctx context.Context, userID int, limit int, beforeID int) ([]*mediadomain.Media, error)
}

MediaService is the narrow slice of the media domain service the agent handlers depend on. *mediadomain.Service satisfies it. It is exported so mockery can generate an exported constructor callable from the package's *_test files (CLAUDE.md mandates *_test packages, which cannot reach an unexported mock).

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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