diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..50e1af2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# Plugin SDK + +Go module `git.dev.alexdunmow.com/block/pluginsdk`. + +**Purpose: this is the ONE plugin-facing artifact.** BlockNinja CMS plugins +import this module and nothing else from the internal Go tree. Plugins must +NOT import `block/core` — core exists only for code shared between first-party +entities (cms, orchestrator, ninja CLI). + +## The contract is proto, not Go + +- `abi/proto/v1/*.proto` is **the contract**. The host↔guest wire is proto-only; + a plugin in ANY language generates its own bindings from these files. +- `abi/v1/` is the **Go** binding of that contract (`abiv1`). The rest of the Go + surface here — `plugin/`, `plugin/wasmguest/**`, and the guest-facing type + packages — is **one language binding** of the SDK. A non-Go plugin implements + the ABI hooks directly and never sees these Go structs. + +## Critical Rules + +- **NEVER use `replace` directives in go.mod** — not here, not in any consumer. + All module resolution goes through the Gitea module proxy. To test local + changes, tag and push a version. +- All consumers are in-house — **no backwards-compatibility shims**. Change the + API and update consumers. +- Regenerate Go bindings after editing any `.proto`: `make proto`. + +## `templates/bn/` is a synced copy — do not author here + +The bn page chrome (head / toolbar / engagement / asset_hooks) is **authored in +`cms/backend/templates/bn`** and mechanically copied here via cms `make +sync-templates`, solely so guest-side plugin templates can compile it into their +wasm. check-safety **check 31** fails cms commits while the copies drift. Never +edit these files here directly — change them in cms and sync. (This chrome is +transitional: it is deleted once host-side head/body injection lands — Phase 3.) + +## Design + +Program spec (in the cms repo): +`docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d115c60 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +SDK_MODULE := git.dev.alexdunmow.com/block/pluginsdk + +# Lint + regenerate the Go bindings for the wasm plugin ABI from the proto +# tree (abi/proto/v1). go_package is set explicitly in each .proto; with +# paths=source_relative and out: ., v1/*.proto emits to abi/v1/*.pb.go, i.e. +# Go package git.dev.alexdunmow.com/block/pluginsdk/abi/v1 (abiv1). +.PHONY: proto +proto: + cd abi && buf lint && buf generate + +.PHONY: build +build: + go build ./... + +.PHONY: test +test: + go test ./... + +.PHONY: tidy +tidy: + go mod tidy diff --git a/README.md b/README.md index 01c70b4..b9f48fb 100644 --- a/README.md +++ b/README.md @@ -1 +1,46 @@ -# BlockNinja Plugin SDK (block/pluginsdk) +# block/pluginsdk + +The **plugin-facing SDK** for BlockNinja CMS plugins. Plugins import THIS module, +never `block/core`. + +## What lives here + +- **`abi/proto/v1/`** — THE single source-of-truth ABI proto tree for the wasm + plugin contract. The wire between host and guest is proto-only; any language + can generate its own bindings from these `.proto` files. (This tree used to be + duplicated by hand in `cms/backend/abi` and `core/abi`; it now lives here once.) +- **`abi/v1/`** — the generated Go bindings (`abiv1`). Regenerate with `make proto`. +- **`plugin/`** — the Go registration + DI surface (`PluginRegistration`, + `CoreServices`/`ServiceDeps`, block/template registries) and the minimal Go + wasm transport shim under `plugin/wasmguest/` (exports, dispatch, hostcalls, + context rehydration), its capability stubs (`plugin/wasmguest/caps/`), and the + `database/sql` driver over the host DB (`plugin/wasmguest/bnwasm/`). +- **Guest-facing type packages** plugins consume: `blocks/` (incl. `blocks/builtin`, + `blocks/shared`, `blocks/tags`), `templates/` (registry + `templates/pongo`), + `templates/bn/` (the templ chrome — see sync rule below), `auth/`, `settings/`, + `content/`, `gating/`, `crypto/`, `rbac/`, `video/`, `ai/`, `subscriptions/`, + `menus/`, `datasources/`. + +The Go surface here is **one language binding**. The proto tree is the contract: +a non-Go plugin implements the ABI hooks directly and never sees the Go structs. + +## `templates/bn` synced-copy rule + +`templates/bn/*` is **authored in the cms repo** (`cms/backend/templates/bn`) and +**synced into here** via cms `make sync-templates`. Do not hand-edit the copy in +this repo — edit it in cms and re-sync. Drift is enforced by check-safety **check 31** +(`check-safety/check_templatesync.go`), which compares this repo's `templates/bn` +against the cms authoritative copy. (This chrome is transitional: it is deleted from +the SDK once host-side head/body injection lands — Phase 3.) + +## Rules + +- **NEVER use `replace` directives** in `go.mod`. Module resolution goes through the + Gitea module proxy — to test local changes, tag and push a version. +- All consumers are in-house — **no backwards-compatibility shims**. +- Plugins import `block/pluginsdk/...`, never `block/core/...`. + +## Design + +Program spec (in the cms repo): +`docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md`. diff --git a/abi/buf.gen.yaml b/abi/buf.gen.yaml new file mode 100644 index 0000000..6fc6a81 --- /dev/null +++ b/abi/buf.gen.yaml @@ -0,0 +1,12 @@ +# Codegen for the wasm plugin ABI (WO-WZ-001). +# +# go_package is set explicitly in each .proto (managed mode not needed — +# unlike the proto/ submodule, we own these files). With +# paths=source_relative and out: ., v1/*.proto emits to abi/v1/*.pb.go, +# i.e. Go package git.dev.alexdunmow.com/block/pluginsdk/abi/v1 (abiv1). +version: v2 +plugins: + - local: protoc-gen-go + out: . + opt: + - paths=source_relative diff --git a/abi/buf.yaml b/abi/buf.yaml new file mode 100644 index 0000000..e714d15 --- /dev/null +++ b/abi/buf.yaml @@ -0,0 +1,21 @@ +# Buf module for the wasm plugin ABI (WO-WZ-001). +# +# Deliberately separate from the repo-root buf.yaml: that one wraps the +# proto/ git submodule (shared block/proto service contracts), while this +# ABI is SDK-internal and versions in lockstep with the guest shim, so it +# lives repo-local under abi/proto. Run buf from this directory (`make abi` +# at the repo root). +version: v2 +modules: + - path: proto +lint: + use: + - STANDARD + except: + # The WO/spec fix the schema path at abi/proto/v1/ (module-relative v1/) + # while the proto package is abi.v1, so the package↔directory rule + # cannot hold without an abi/proto/abi/v1 stutter. + - PACKAGE_DIRECTORY_MATCH +breaking: + use: + - FILE diff --git a/abi/proto/v1/capability.proto b/abi/proto/v1/capability.proto new file mode 100644 index 0000000..ae387e5 --- /dev/null +++ b/abi/proto/v1/capability.proto @@ -0,0 +1,844 @@ +// capability.proto — guest→host capability calls (WO-WZ-001). +// +// Each CoreServices interface (core/plugin/deps.go) becomes a host-function +// family; every interface method gets one request/response message pair +// here, mirroring the Go signature 1:1. UUIDs travel as canonical strings; +// map[string]any / []byte-JSON values travel as JSON bytes. +// +// Calls cross via the generic HostCallRequest/HostCallResponse envelope; the +// method string selects the pair (e.g. "content.get_author_profile" → +// ContentGetAuthorProfileRequest/Response). Full method table in +// core/docs/wasm-abi.md. +// +// Not represented here by design: +// - CoreServices.Pool → db.proto (the DB driver messages) +// - CoreServices.Interceptors → host-side only (never crosses) +// - CoreServices.AppURL/MediaPath → LoadRequest.host_config (invoke.proto) +// - CoreServices.CoreServiceBindings → manifest core_service_bindings +// - RAGService.RegisterContentFetcher → manifest rag_content_fetcher_types +// + the RAG_FETCH hook (callback inversion) +// - ai.ToolDefinition.Handler → host→guest tool execution is a runtime-WO +// concern (flagged in core/docs/wasm-abi.md) + +syntax = "proto3"; + +package abi.v1; + +import "google/protobuf/timestamp.proto"; +import "v1/invoke.proto"; + +option go_package = "git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1"; + +// HostCallRequest is the generic guest→host capability envelope. +message HostCallRequest { + // Capability method, "." (e.g. "crypto.encrypt_secret"). + string method = 1; + // Serialized family request message. + bytes payload = 2; +} + +// HostCallResponse is the generic host→guest capability return envelope. +message HostCallResponse { + // Serialized family response message; empty when error is set. + bytes payload = 1; + // Set when the capability call failed; the guest SDK surfaces it as a + // normal Go error. + AbiError error = 2; +} + +// --- content.* (content.Content) --- + +message ContentGetAuthorProfileRequest { + string id = 1; // UUID +} + +message ContentGetAuthorProfileResponse { + AuthorProfile author = 1; +} + +// AuthorProfile mirrors content.AuthorProfile. +message AuthorProfile { + string id = 1; // UUID + string name = 2; + string slug = 3; + string bio = 4; + string avatar_url = 5; + string website = 6; + map social_links = 7; +} + +message ContentGetPageRequest { + string slug = 1; +} + +message ContentGetPageResponse { + PageInfo page = 1; +} + +// PageInfo mirrors content.PageInfo. +message PageInfo { + string id = 1; // UUID + string slug = 2; + string title = 3; +} + +message ContentGetPostRequest { + string slug = 1; +} + +message ContentGetPostResponse { + PostInfo post = 1; +} + +// PostInfo mirrors content.PostInfo. +message PostInfo { + string id = 1; // UUID + string slug = 2; + string title = 3; + string excerpt = 4; + string featured_image_url = 5; + string author_id = 6; // UUID + string body = 7; // rendered HTML; set by get_post / list_posts(include_body) + string author_name = 8; + string author_slug = 9; + google.protobuf.Timestamp published_at = 10; +} + +message ContentListPostsRequest { + int32 limit = 1; + int32 offset = 2; + string category = 3; // optional category slug filter ("" = all) + bool published_only = 4; + bool include_body = 5; + bool include_excerpt = 6; +} + +message ContentListPostsResponse { + repeated PostInfo posts = 1; +} + +message ContentSlugifyRequest { + string text = 1; +} + +message ContentSlugifyResponse { + string slug = 1; +} + +message ContentBlockNoteToHtmlRequest { + // JSON encoding of the BlockNote document map. + bytes doc_json = 1; +} + +message ContentBlockNoteToHtmlResponse { + string html = 1; +} + +message ContentGenerateExcerptRequest { + string html = 1; + int32 max_len = 2; +} + +message ContentGenerateExcerptResponse { + string excerpt = 1; +} + +message ContentStripHtmlRequest { + string html = 1; +} + +message ContentStripHtmlResponse { + string text = 1; +} + +// --- settings.* (settings.Settings) + settings.update (settings.Updater) --- + +message SettingsGetSiteSettingsRequest {} + +message SettingsGetSiteSettingsResponse { + // JSON encoding of the site settings map. + bytes settings_json = 1; +} + +message SettingsGetPluginSettingsRequest { + string plugin_name = 1; +} + +message SettingsGetPluginSettingsResponse { + // JSON encoding of the plugin settings map. + bytes settings_json = 1; +} + +message SettingsUpdateSiteSettingRequest { + string key = 1; + // JSON encoding of the value. + bytes value_json = 2; +} + +message SettingsUpdateSiteSettingResponse {} + +// --- gating.* (gating.Gating) --- + +message GatingGetSubscriberTierLevelRequest { + string user_id = 1; // UUID +} + +message GatingGetSubscriberTierLevelResponse { + int32 level = 1; +} + +message GatingEvaluateAccessRequest { + int32 user_tier_level = 1; + // Absent rule means "no rule" (access granted). + AccessRule rule = 2; +} + +message GatingEvaluateAccessResponse { + AccessResult result = 1; +} + +// AccessRule mirrors gating.AccessRule. +message AccessRule { + int32 min_tier_level = 1; + string override_tier_id = 2; + string teaser_mode = 3; // "hard", "soft", "none" + int32 teaser_percent = 4; +} + +// AccessResult mirrors gating.AccessResult. +message AccessResult { + bool has_access = 1; + string teaser_mode = 2; + int32 teaser_percent = 3; + int32 required_level = 4; +} + +// --- crypto.* (crypto.Crypto) --- + +message CryptoEncryptSecretRequest { + string plaintext = 1; +} + +message CryptoEncryptSecretResponse { + string ciphertext = 1; +} + +message CryptoDecryptSecretRequest { + string ciphertext = 1; +} + +message CryptoDecryptSecretResponse { + string plaintext = 1; +} + +// --- menus.* (menus.Menus) --- + +message MenusGetMenuByNameRequest { + string name = 1; +} + +message MenusGetMenuByNameResponse { + Menu menu = 1; +} + +// Menu mirrors menus.Menu. +message Menu { + string id = 1; // UUID + string name = 2; +} + +message MenusGetMenuItemsRequest { + string menu_id = 1; // UUID +} + +message MenusGetMenuItemsResponse { + repeated MenuItem items = 1; +} + +// MenuItem mirrors menus.MenuItem. +message MenuItem { + string id = 1; // UUID + string menu_id = 2; // UUID + string label = 3; + string url = 4; + string page_slug = 5; + optional string parent_id = 6; // UUID; unset mirrors nil *uuid.UUID + int32 sort_order = 7; + bool open_in_new_tab = 8; + string css_class = 9; + string item_type = 10; + string icon = 11; +} + +// --- datasources.* (datasources.Datasources) --- + +message DatasourcesResolveBucketRequest { + string bucket_id = 1; // UUID +} + +message DatasourcesResolveBucketResponse { + DatasourceResult result = 1; +} + +message DatasourcesResolveBucketByKeyRequest { + string bucket_key = 1; +} + +message DatasourcesResolveBucketByKeyResponse { + DatasourceResult result = 1; +} + +// DatasourceResult mirrors datasources.Result ([]any / map[string]any as +// JSON bytes). +message DatasourceResult { + // JSON array of items. + bytes items_json = 1; + int32 total = 2; + // JSON object; empty when absent. + bytes meta_json = 3; +} + +// --- users.* (auth.PublicUsers) --- + +message UsersGetByUsernameRequest { + string username = 1; +} + +message UsersGetByUsernameResponse { + PublicUserProfile user = 1; +} + +message UsersGetByIdRequest { + string id = 1; // UUID +} + +message UsersGetByIdResponse { + PublicUserProfile user = 1; +} + +// PublicUserProfile mirrors auth.PublicUserProfile. +message PublicUserProfile { + string id = 1; // UUID + string email = 2; + string username = 3; + string display_name = 4; + string avatar_url = 5; + string bio = 6; + bool email_verified = 7; + string role = 8; +} + +// --- subscriptions.* (subscriptions.Subscriptions) --- + +message SubscriptionsGetUserTierLevelRequest { + string user_id = 1; // UUID +} + +message SubscriptionsGetUserTierLevelResponse { + TierLevel tier_level = 1; +} + +// TierLevel mirrors subscriptions.TierLevel. +message TierLevel { + int32 level = 1; + // JSON feature payload. + bytes features = 2; +} + +message SubscriptionsGetTierBySlugRequest { + string slug = 1; +} + +message SubscriptionsGetTierBySlugResponse { + Tier tier = 1; +} + +// Tier mirrors subscriptions.Tier. +message Tier { + string id = 1; // UUID + string name = 2; + string slug = 3; + int32 level = 4; + string description = 5; + // JSON feature payload. + bytes features = 6; + bool is_default = 7; + int32 position = 8; +} + +message SubscriptionsListTiersRequest {} + +message SubscriptionsListTiersResponse { + repeated Tier tiers = 1; +} + +message SubscriptionsListActivePlansRequest { + string tier_id = 1; // UUID +} + +message SubscriptionsListActivePlansResponse { + repeated Plan plans = 1; +} + +// Plan mirrors subscriptions.Plan. +message Plan { + string id = 1; // UUID + string tier_id = 2; // UUID + string billing_interval = 3; + int32 amount = 4; + string currency = 5; + bool is_active = 6; + google.protobuf.Timestamp created_at = 7; +} + +// --- media.deposit (plugin.Media) --- + +// MediaDepositRequest mirrors plugin.MediaDeposit. +message MediaDepositRequest { + // Optional deterministic media row ID (UUID); empty = host generates one. + string id = 1; + string filename = 2; + bytes data = 3; + string alt_text = 4; + string folder = 5; + string source = 6; +} + +// MediaDepositResponse mirrors plugin.MediaResult. +message MediaDepositResponse { + string id = 1; // UUID + // Ready-to-use reference ("media:"). + string ref = 2; + bool created = 3; +} + +// --- email.send (plugin.EmailSender) --- + +message EmailSendRequest { + string to = 1; + string subject = 2; + string body = 3; +} + +message EmailSendResponse {} + +// --- ai.text_call (CoreServices.AITextCall) --- + +message AiTextCallRequest { + string task_key = 1; + string system_prompt = 2; + string user_message = 3; +} + +message AiTextCallResponse { + string text = 1; +} + +// --- ai.tools.* (ai.ToolRegistry) --- + +// AiToolRegisterRequest mirrors ai.ToolDefinition (minus Handler, which +// stays guest-side; execution direction is a runtime-WO concern). +message AiToolRegisterRequest { + string slug = 1; + string name = 2; + string description = 3; + // JSON encoding of the parameter schema map. + bytes parameter_schema_json = 4; +} + +message AiToolRegisterResponse {} + +// --- bridge.* (plugin.PluginBridge) --- +// +// The bridge shares in-process Go values today; across sandboxes only the +// registration/lookup surface serializes. Typed cross-plugin calls need a +// runtime design (see core/docs/wasm-abi.md, open items). + +message BridgeRegisterServiceRequest { + string plugin_name = 1; + string service_name = 2; +} + +message BridgeRegisterServiceResponse {} + +message BridgeGetServiceRequest { + string plugin_name = 1; + string service_name = 2; +} + +message BridgeGetServiceResponse { + bool available = 1; +} + +// --- jobs.submit (plugin.JobRunner) --- + +message JobsSubmitRequest { + string job_type = 1; + // JSON job configuration. + bytes config_json = 2; +} + +message JobsSubmitResponse {} + +// --- embeddings.* (plugin.EmbeddingService) --- + +message EmbeddingsGenerateEmbeddingRequest { + string text = 1; +} + +message EmbeddingsGenerateEmbeddingResponse { + repeated float embedding = 1; +} + +message EmbeddingsEmbedContentRequest { + string source_type = 1; + string source_id = 2; // UUID + string text = 3; +} + +message EmbeddingsEmbedContentResponse { + bool embedded = 1; +} + +message EmbeddingsIsAvailableRequest {} + +message EmbeddingsIsAvailableResponse { + bool available = 1; +} + +// --- rag.* (plugin.RAGService) --- + +message RagQueryRequest { + string query = 1; + int32 limit = 2; +} + +message RagQueryResponse { + repeated RagResult results = 1; +} + +// RagResult mirrors plugin.RAGResult. +message RagResult { + string content = 1; + double score = 2; + map metadata = 3; +} + +message RagOnContentChangedRequest { + string content_type = 1; + string content_id = 2; // UUID +} + +message RagOnContentChangedResponse {} + +// --- reviews.* (plugin.ReviewSubmitter) --- + +// ReviewsSubmitReviewRequest mirrors plugin.SubmitReviewParams. +message ReviewsSubmitReviewRequest { + string table_id = 1; // UUID + string row_id = 2; // UUID + int32 overall_rating = 3; + string review_text = 4; + // JSON encoding of the per-criterion ratings map. + bytes ratings_json = 5; + repeated string photos = 6; +} + +message ReviewsSubmitReviewResponse { + string review_id = 1; +} + +// --- badges.refresh (plugin.BadgeRefresher) --- + +message BadgesRefreshBadgesRequest { + string table_id = 1; // UUID + string row_id = 2; // UUID +} + +message BadgesRefreshBadgesResponse {} + +// --- content.* writes (content.Author, WO-WZ-019) --- +// +// Imperative page/post authoring. The idempotent seed-time counterparts live +// in the provisioner.* family below; these are for runtime authoring and for +// the WO-WZ-020 codeless seed runner. + +message ContentCreatePageRequest { + string slug = 1; + string parent_slug = 2; // "" = root + string title = 3; + string template_key = 4; + string master_page_key = 5; // "" = none +} + +message ContentCreatePageResponse { + string page_id = 1; // UUID + // False when a page with this slug already existed (the existing ID is + // returned and nothing is modified). + bool created = 2; +} + +// PageBlock is one block instance placed on a page (mirrors +// plugin.PageBlockConfig / MasterPageBlock). +message PageBlock { + string block_key = 1; + string title = 2; + // JSON encoding of the block's content map. + bytes content_json = 3; + optional string html_content = 4; + string slot = 5; + int32 sort_order = 6; +} + +message ContentSetPageBlocksRequest { + string page_id = 1; // UUID + // Full replacement set for the page's draft document blocks. + repeated PageBlock blocks = 2; +} + +message ContentSetPageBlocksResponse {} + +message ContentPublishPageRequest { + string page_id = 1; // UUID +} + +message ContentPublishPageResponse {} + +// ContentSetPageSeoRequest mirrors the CMS UpdatePageSEO surface. Unset +// optionals leave the existing value untouched. +message ContentSetPageSeoRequest { + string page_id = 1; // UUID + optional string meta_title = 2; + optional string meta_description = 3; + optional string og_title = 4; + optional string og_description = 5; + optional string og_image = 6; + optional string focus_keyphrase = 7; + optional string canonical_url = 8; + optional string robots_directive = 9; + optional string twitter_title = 10; + optional string twitter_description = 11; + optional string twitter_image = 12; +} + +message ContentSetPageSeoResponse {} + +// ContentUpsertPostRequest creates or updates a blog post by slug (posts live +// in the dedicated blog_posts table). +message ContentUpsertPostRequest { + string slug = 1; + string title = 2; + // JSON encoding of the BlockNote document. + bytes document_json = 3; + optional string excerpt = 4; + optional string author_profile_id = 5; // UUID + optional string featured_image_id = 6; // UUID (e.g. from media.deposit) + // Publish immediately after the upsert. + bool publish = 7; + bool is_featured = 8; +} + +message ContentUpsertPostResponse { + string post_id = 1; // UUID + bool created = 2; +} + +// --- provisioner.* (plugin.Provisioner, WO-WZ-019) --- +// +// Idempotent seed/ensure operations, 1:1 with the plugin.Provisioner Go +// interface. In the wasm world these are LOAD-TIME capabilities: plugins call +// deps.Provisioner from their Load hook (RegisterWithProvisioner cannot cross +// the DESCRIBE boundary — host functions are stubbed there). All methods +// check-then-create; re-running them is safe. +// +// EmbedConfig.RenderFunc deliberately does not cross: an ABI-provisioned +// embed is template-rendered host-side. A compute-rendered embed needs a +// block + hook instead. + +message ProvisionerEnsureDataTableRequest { + string key = 1; + string name = 2; + string description = 3; + // JSON schema of the table (json.RawMessage in DataTableConfig). + bytes schema_json = 4; + string primary_key = 5; +} + +message ProvisionerEnsureDataTableResponse {} + +message ProvisionerMergeSiteSettingsRequest { + // JSON encoding of the defaults map; existing keys win. + bytes defaults_json = 1; +} + +message ProvisionerMergeSiteSettingsResponse {} + +message ProvisionerEnsureSettingRequest { + string key = 1; + // JSON encoding of the default value. + bytes default_value_json = 2; +} + +message ProvisionerEnsureSettingResponse {} + +// PageSeed mirrors plugin.PageConfig. +message PageSeed { + string slug = 1; + string parent_slug = 2; + string title = 3; + string template_key = 4; + repeated PageBlock blocks = 5; + string detail_source_type = 6; + string detail_source_key = 7; + string detail_slug_field = 8; + bool reconcile_blocks = 9; + bool reconcile_template = 10; +} + +message ProvisionerEnsurePageRequest { + PageSeed page = 1; +} + +message ProvisionerEnsurePageResponse {} + +message ProvisionerOverrideSiteSettingsRequest { + // JSON encoding of the overrides map; plugin values win. + bytes overrides_json = 1; +} + +message ProvisionerOverrideSiteSettingsResponse {} + +// ProvisionerEnsureMenuItemRequest mirrors plugin.MenuItemConfig under a +// named menu. +message ProvisionerEnsureMenuItemRequest { + string menu_name = 1; + string label = 2; + string url = 3; + string page_slug = 4; + int32 sort_order = 5; +} + +message ProvisionerEnsureMenuItemResponse {} + +// ProvisionerRegisterEmbeddingConfigRequest mirrors plugin.EmbeddingConfigDef. +message ProvisionerRegisterEmbeddingConfigRequest { + string table_key = 1; + string text_template = 2; + bool enabled = 3; +} + +message ProvisionerRegisterEmbeddingConfigResponse {} + +// ProvisionerEnsureEmbedRequest mirrors plugin.EmbedConfig minus RenderFunc +// (function values cannot cross; the Template renders host-side). +message ProvisionerEnsureEmbedRequest { + string key = 1; + string title = 2; + string description = 3; + string icon = 4; + string label_field = 5; + string template = 6; + string data_source_type = 7; // EmbedDataSource.Type + string data_source_table_key = 8; // EmbedDataSource.TableKey +} + +message ProvisionerEnsureEmbedResponse {} + +// ProvisionerEnsureJobScheduleRequest mirrors plugin.JobScheduleConfig. +message ProvisionerEnsureJobScheduleRequest { + string job_type = 1; + string cron_expression = 2; + // JSON job configuration. + bytes config_json = 3; +} + +message ProvisionerEnsureJobScheduleResponse {} + +message ProvisionerUpdateDataTableRowFieldRequest { + string row_id = 1; // UUID + string field_key = 2; + // JSON encoding of the value. + bytes value_json = 3; +} + +message ProvisionerUpdateDataTableRowFieldResponse {} + +message ProvisionerDisableOrphanedJobSchedulesRequest { + repeated string registered_types = 1; +} + +message ProvisionerDisableOrphanedJobSchedulesResponse {} + +message ProvisionerEnsurePluginRequest { + string name = 1; +} + +message ProvisionerEnsurePluginResponse {} + +// ProvisionerEnsureCustomColorRequest mirrors plugin.CustomColorConfig. +message ProvisionerEnsureCustomColorRequest { + string name = 1; + string light_value = 2; + string dark_value = 3; + string source = 4; +} + +message ProvisionerEnsureCustomColorResponse {} + +// ProvisionerEnsureMediaRequest mirrors plugin.MediaDeposit with a REQUIRED +// deterministic id (the template-referable key). Idempotent: an existing id +// is a no-op; changed bytes warn rather than overwrite. +message ProvisionerEnsureMediaRequest { + string id = 1; // UUID, required + string filename = 2; + bytes data = 3; + string alt_text = 4; + string folder = 5; + string source = 6; +} + +message ProvisionerEnsureMediaResponse {} + +// --- settings.update_plugin_settings (settings.Updater, WO-WZ-019) --- + +// SettingsUpdatePluginSettingsRequest replaces the calling plugin's own +// settings map. The host binds the target plugin from the caller's identity; +// plugin_name is advisory and MUST match it (PERMISSION_DENIED otherwise). +message SettingsUpdatePluginSettingsRequest { + string plugin_name = 1; + // JSON encoding of the full settings map. + bytes settings_json = 2; +} + +message SettingsUpdatePluginSettingsResponse {} + +// --- jobs.progress (plugin.JobHandlerFunc progress callback, WO-WZ-019) --- + +// JobsProgressRequest reports progress for the CURRENTLY EXECUTING job. The +// host correlates it to the job via the invoking HOOK_JOB call's context, so +// it is only meaningful while a JOB hook is on the stack; outside one it is +// accepted and discarded. +message JobsProgressRequest { + int32 current = 1; + int32 total = 2; + string message = 3; +} + +message JobsProgressResponse {} + +// --- bridge.invoke (plugin.PluginBridge cross-plugin calls, WO-WZ-019) --- + +// BridgeInvokeRequest calls a method on another plugin's registered bridge +// service. The provider answers via HOOK_BRIDGE_CALL (wasm) or an in-process +// plugin.BridgeInvokable (bundled). Payloads are opaque bytes — the two +// plugins agree on the encoding (JSON by convention). +message BridgeInvokeRequest { + string plugin_name = 1; + string service_name = 2; + string method = 3; + bytes payload = 4; +} + +message BridgeInvokeResponse { + bytes payload = 1; +} diff --git a/abi/proto/v1/db.proto b/abi/proto/v1/db.proto new file mode 100644 index 0000000..3d2386b --- /dev/null +++ b/abi/proto/v1/db.proto @@ -0,0 +1,119 @@ +// db.proto — DB driver messages (WO-WZ-001). +// +// The guest SDK ships a database/sql driver that marshals query text + args +// out through these messages and rows back, so sqlc-generated plugin code +// works unchanged. The host executes on a connection under the per-plugin +// Postgres role. Transactions map to a host-side handle held per guest call +// chain, with a hard deadline so a guest can never pin a connection. + +syntax = "proto3"; + +package abi.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1"; + +// DbValue is one pgx-mappable parameter or column value. +message DbValue { + oneof kind { + // SQL NULL (the bool carries no information; true by convention). + bool null = 1; + bool bool_value = 2; + int64 int64_value = 3; + double float64_value = 4; + string string_value = 5; + bytes bytes_value = 6; + google.protobuf.Timestamp timestamp_value = 7; + // UUID in canonical string form. + string uuid_value = 8; + // JSON/JSONB payload bytes. + bytes jsonb_value = 9; + // NUMERIC in decimal string form (lossless). + string numeric_value = 10; + // text[] array. + TextArray text_array_value = 11; + // uuid[] array (each element canonical string form). Distinct from + // text_array so the host binds a native uuid[] parameter (letting a query + // keep `ANY($1::uuid[])` with no text[]-cast workaround) and scans a uuid[] + // column straight into []uuid.UUID. + UuidArray uuid_array_value = 12; + } +} + +// TextArray is a Postgres text[] value. +message TextArray { + repeated string values = 1; +} + +// UuidArray is a Postgres uuid[] value; each element is a canonical UUID +// string (matching DbValue.uuid_value's single-UUID encoding). +message UuidArray { + repeated string values = 1; +} + +// DbRow is one result row; values align with DbRowsResponse.columns. +message DbRow { + repeated DbValue values = 1; +} + +// DbError carries a database failure back to the guest driver. +message DbError { + // Postgres SQLSTATE when available (e.g. "23505"); empty otherwise. + string code = 1; + string message = 2; +} + +// DbQueryRequest executes a rows-returning statement. +message DbQueryRequest { + string sql = 1; + repeated DbValue args = 2; + // Transaction handle from DbTxBeginResponse; 0 = no transaction + // (autocommit). + uint64 tx_handle = 3; +} + +// DbRowsResponse returns the full buffered result set. +message DbRowsResponse { + repeated string columns = 1; + repeated DbRow rows = 2; + DbError error = 3; +} + +// DbExecRequest executes a statement without returning rows. +message DbExecRequest { + string sql = 1; + repeated DbValue args = 2; + // Transaction handle from DbTxBeginResponse; 0 = no transaction. + uint64 tx_handle = 3; +} + +message DbExecResponse { + int64 rows_affected = 1; + DbError error = 2; +} + +// DbTxBeginRequest opens a host-side transaction for this call chain. +message DbTxBeginRequest {} + +message DbTxBeginResponse { + // Opaque handle referencing the host-side transaction; never 0 on success. + uint64 tx_handle = 1; + DbError error = 2; +} + +message DbTxCommitRequest { + uint64 tx_handle = 1; +} + +message DbTxCommitResponse { + DbError error = 1; +} + +message DbTxRollbackRequest { + uint64 tx_handle = 1; +} + +message DbTxRollbackResponse { + DbError error = 1; +} diff --git a/abi/proto/v1/http.proto b/abi/proto/v1/http.proto new file mode 100644 index 0000000..43521f0 --- /dev/null +++ b/abi/proto/v1/http.proto @@ -0,0 +1,37 @@ +// http.proto — buffered HTTP request/response payloads for the HANDLE_HTTP +// hook (WO-WZ-001). +// +// v1 buffers full bodies: no streaming, SSE, or WebSockets inside plugins +// (per the wasm migration design spec §5). The guest runs its real +// chi/connect mux internally and answers one HttpRequest with one +// HttpResponse. + +syntax = "proto3"; + +package abi.v1; + +option go_package = "git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1"; + +// HttpRequest is a fully buffered HTTP request forwarded to the guest mux. +message HttpRequest { + string method = 1; + // Request path (no scheme/host/query). + string path = 2; + // Raw query string (without the leading '?'). + string raw_query = 3; + // Canonical header name → values. + map headers = 4; + bytes body = 5; +} + +// HeaderValues holds the values of one multi-valued HTTP header. +message HeaderValues { + repeated string values = 1; +} + +// HttpResponse is the guest's fully buffered response. +message HttpResponse { + int32 status = 1; + map headers = 2; + bytes body = 3; +} diff --git a/abi/proto/v1/invoke.proto b/abi/proto/v1/invoke.proto new file mode 100644 index 0000000..9e06563 --- /dev/null +++ b/abi/proto/v1/invoke.proto @@ -0,0 +1,278 @@ +// invoke.proto — the bn_invoke envelope, hook catalog, and the job/lifecycle +// hook payloads (WO-WZ-001). +// +// Every host→guest call crosses the wasm boundary as one guest export: +// +// bn_invoke(hook_id, ptr, len) → packed(ptr, len) +// +// where (ptr, len) frames a serialized InvokeRequest and the packed return +// frames a serialized InvokeResponse. Hook-specific payloads (render.proto, +// http.proto, and the messages below) travel inside InvokeRequest.payload / +// InvokeResponse.payload. See core/docs/wasm-abi.md. + +syntax = "proto3"; + +package abi.v1; + +import "v1/manifest.proto"; + +option go_package = "git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1"; + +// Hook identifies the guest entry point being invoked. +enum Hook { + HOOK_UNSPECIFIED = 0; + // Render one block: payload = RenderBlockRequest / RenderBlockResponse. + HOOK_RENDER_BLOCK = 1; + // Render one template: payload = RenderTemplateRequest / RenderTemplateResponse. + HOOK_RENDER_TEMPLATE = 2; + // Forward a buffered HTTP request to the guest's mux: + // payload = HttpRequest / HttpResponse. + HOOK_HANDLE_HTTP = 3; + // Run a background job handler: payload = JobRequest / JobResponse. + HOOK_JOB = 4; + // Plugin load lifecycle: payload = LoadRequest / LoadResponse. + HOOK_LOAD = 5; + // Plugin unload lifecycle: payload = UnloadRequest / UnloadResponse. + HOOK_UNLOAD = 6; + // Re-fetch content for RAG re-indexing: + // payload = RagFetchRequest / RagFetchResponse. + HOOK_RAG_FETCH = 7; + // Media lifecycle event delivery: payload = MediaHookRequest / MediaHookResponse. + HOOK_MEDIA_HOOK = 8; + // Capture the static manifest at publish time: + // payload = DescribeRequest / DescribeResponse. + HOOK_DESCRIBE = 9; + // Invoke a plugin-declared template tag (manifest.declared_tags) that the + // host engine hit while rendering a powered block: + // payload = RenderTagRequest / RenderTagResponse. + HOOK_RENDER_TAG = 10; + // Apply a plugin-declared template filter (manifest.declared_filters): + // payload = ApplyFilterRequest / ApplyFilterResponse. + HOOK_APPLY_FILTER = 11; + // Execute a guest-registered AI tool handler (ai.tools.register): + // payload = AiToolCallRequest / AiToolCallResponse. + HOOK_AI_TOOL_CALL = 12; + // Invoke a method on a bridge service this plugin registered + // (bridge.register_service): payload = BridgeCallRequest / BridgeCallResponse. + HOOK_BRIDGE_CALL = 13; + // Render one directory panel section (DirectoryExtensions.PanelSections): + // payload = DirectoryPanelSectionRequest / DirectoryPanelSectionResponse. + HOOK_DIRECTORY_PANEL_SECTION = 14; + // Run one directory pin decorator (DirectoryExtensions.PinDecorators): + // payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse. + HOOK_DIRECTORY_PIN_DECORATOR = 15; +} + +// InvokeRequest is the host→guest call envelope. +message InvokeRequest { + Hook hook = 1; + // Serialized hook-specific request message (see Hook value comments). + bytes payload = 2; + // Milliseconds the guest has to answer; the host also enforces this + // deadline on the wasm instance (WithCloseOnContextDone). + int64 deadline_ms = 3; +} + +// InvokeResponse is the guest→host return envelope. +message InvokeResponse { + // Serialized hook-specific response message; empty when error is set. + bytes payload = 1; + // Set when the hook failed; the host treats decode failures and traps as + // implicit ABI_ERROR_CODE_INTERNAL. + AbiError error = 2; +} + +// AbiErrorCode classifies boundary-crossing failures. +enum AbiErrorCode { + ABI_ERROR_CODE_UNSPECIFIED = 0; + // The handler ran and failed; message carries the Go error text. + ABI_ERROR_CODE_INTERNAL = 1; + // The payload could not be decoded. + ABI_ERROR_CODE_DECODE = 2; + // The hook/capability is not implemented by the callee. + ABI_ERROR_CODE_UNIMPLEMENTED = 3; + // The call exceeded its deadline; the instance is considered poisoned. + ABI_ERROR_CODE_DEADLINE_EXCEEDED = 4; + // The caller is not entitled to this capability. + ABI_ERROR_CODE_PERMISSION_DENIED = 5; + // A db.* call named a transaction handle the host has already expired + // (dropped at the call-chain deadline, WO-WZ-007). Distinct from a real + // fault: the unit of work is retryable in a fresh transaction. Emitted by + // the cms dbexec side (adopted separately) and mapped guest-side to + // bnwasm.ErrTxExpired. + ABI_ERROR_CODE_TX_EXPIRED = 6; +} + +// AbiError is the structured error carried by InvokeResponse and +// HostCallResponse. +message AbiError { + AbiErrorCode code = 1; + string message = 2; +} + +// --- DESCRIBE --- + +// DescribeRequest asks the guest for its static manifest (publish time only). +message DescribeRequest { + // ABI major version of the calling host/publish tool. + uint32 host_abi_version = 1; +} + +// DescribeResponse returns the static manifest. +message DescribeResponse { + PluginManifest manifest = 1; +} + +// --- LOAD / UNLOAD --- + +// LoadRequest carries the static host configuration the .so world exposed as +// CoreServices.AppURL / CoreServices.MediaPath. +message LoadRequest { + HostConfig host_config = 1; +} + +// HostConfig mirrors the plain-value CoreServices fields. +message HostConfig { + string app_url = 1; // CoreServices.AppURL + string media_path = 2; // CoreServices.MediaPath +} + +message LoadResponse {} + +message UnloadRequest {} + +message UnloadResponse {} + +// --- JOB --- + +// JobRequest dispatches one background job to the guest handler registered +// for job_type (manifest.job_types). +message JobRequest { + string job_type = 1; + // JSON job configuration (json.RawMessage in JobHandlerFunc). + bytes config_json = 2; +} + +// JobResponse returns the handler's JSON result. +message JobResponse { + bytes result_json = 1; +} + +// --- RAG_FETCH --- + +// RagFetchRequest asks the guest's registered content fetcher +// (manifest.rag_content_fetcher_types) for a content item's text. +message RagFetchRequest { + string content_type = 1; + string content_id = 2; // UUID +} + +// RagFetchResponse mirrors plugin.ContentFetcher's return values. +message RagFetchResponse { + string title = 1; + string text = 2; +} + +// --- MEDIA_HOOK --- + +// MediaHookRequest delivers one media lifecycle event +// (plugin.MediaHooksProvider). +message MediaHookRequest { + oneof event { + MediaAnalyzedEvent media_analyzed = 1; // OnMediaAnalyzed + ModerationDecisionEvent moderation_decision = 2; // OnModerationDecision + } +} + +message MediaHookResponse {} + +// MediaAnalyzedEvent mirrors plugin.MediaAnalyzedEvent (UUIDs as strings). +message MediaAnalyzedEvent { + string media_id = 1; + string analysis_id = 2; + string content_hash = 3; + string status = 4; + string source_plugin = 5; + string source_type = 6; + string source_ref_id = 7; + string safe_adult = 8; + string safe_violence = 9; + string safe_racy = 10; +} + +// --- AI_TOOL_CALL --- + +// AiToolCallRequest executes the guest-side Handler of a tool the plugin +// registered via ai.tools.register (ai.ToolHandler). +message AiToolCallRequest { + string slug = 1; + // JSON encoding of the params map. + bytes params_json = 2; +} + +// AiToolCallResponse mirrors ai.ToolResult. A handler-level failure travels +// in error_message (a normal tool outcome the model sees); AbiError stays +// reserved for transport/decode/panic failures. +message AiToolCallResponse { + string content = 1; + string error_message = 2; +} + +// --- BRIDGE_CALL --- + +// BridgeCallRequest asks THIS plugin (the provider) to run a method on one of +// its registered bridge services. The consumer side is the bridge.invoke +// capability; payload encoding is agreed between the two plugins (JSON by +// convention). +message BridgeCallRequest { + string service_name = 1; + string method = 2; + bytes payload = 3; +} + +message BridgeCallResponse { + bytes payload = 1; +} + +// --- DIRECTORY_PANEL_SECTION / DIRECTORY_PIN_DECORATOR --- + +// DirectoryPanelSectionRequest renders the index-th registered panel section +// (DirectoryExtensions.PanelSections[index], counted in +// manifest.directory_extensions.panel_section_count). +message DirectoryPanelSectionRequest { + uint32 index = 1; + // JSON encoding of the row data map. + bytes data_json = 2; +} + +message DirectoryPanelSectionResponse { + string html = 1; +} + +// DirectoryPinDecoratorRequest runs the index-th registered pin decorator, +// which mutates the pin map in place; the mutated pin is returned. +message DirectoryPinDecoratorRequest { + uint32 index = 1; + // JSON encoding of the pin map (mutated by the decorator). + bytes pin_json = 2; + // JSON encoding of the row data map (read-only input). + bytes data_json = 3; +} + +message DirectoryPinDecoratorResponse { + // JSON encoding of the mutated pin map. + bytes pin_json = 1; +} + +// ModerationDecisionEvent mirrors plugin.ModerationDecisionEvent. +message ModerationDecisionEvent { + string media_id = 1; + string analysis_id = 2; + string status = 3; + string previous_status = 4; + string source_plugin = 5; + string source_type = 6; + string source_ref_id = 7; + string moderated_by = 8; + string note = 9; +} diff --git a/abi/proto/v1/manifest.proto b/abi/proto/v1/manifest.proto new file mode 100644 index 0000000..a8a8cdf --- /dev/null +++ b/abi/proto/v1/manifest.proto @@ -0,0 +1,243 @@ +// manifest.proto — the static plugin manifest (WO-WZ-001). +// +// PluginManifest is the wire form of everything that is *static data* in +// core/plugin/registration.go (PluginRegistration). It is produced once at +// publish time by calling the guest's DESCRIBE hook and stored as manifest.pb +// inside the .bnp artifact; the CMS loader reads it without instantiating the +// wasm module. +// +// Function-valued registration fields cannot cross the wire; they map to +// either a hook (Load/Unload/JobHandlers/MediaHooks/HTTPHandler), a boolean +// presence flag here, or an artifact directory (Assets → assets/, +// Schemas → schemas/, Migrations → migrations/). See core/docs/wasm-abi.md +// for the full field-by-field mapping. + +syntax = "proto3"; + +package abi.v1; + +option go_package = "git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1"; + +// PluginManifest mirrors the static surface of plugin.PluginRegistration. +message PluginManifest { + // ABI major version the plugin was built against. The host rejects + // manifests whose major version it does not support. + uint32 abi_version = 1; + + // PluginRegistration.Name / .Version. + string name = 2; + string version = 3; + + // PluginRegistration.Dependencies. + repeated Dependency dependencies = 4; + + // Blocks registered via BlockRegistry.Register (captured by DESCRIBE). + repeated BlockMeta blocks = 5; + // Blocks registered via BlockRegistry.RegisterTemplateOverride[WithSource]. + repeated BlockTemplateOverride block_template_overrides = 6; + + // Templates registered via TemplateRegistry (captured by DESCRIBE). + repeated string template_keys = 7; // TemplateRegistry.Register + repeated SystemTemplateMeta system_templates = 8; // RegisterSystemTemplate + repeated PageTemplateMeta page_templates = 9; // RegisterPageTemplate + repeated string email_wrapper_system_keys = 10; // RegisterEmailWrapper + + // PluginRegistration.AdminPages. + repeated AdminPage admin_pages = 11; + + // PluginRegistration.SettingsSchema / .ThemePresets / .BundledFonts (JSON). + bytes settings_schema = 12; + bytes theme_presets = 13; + bytes bundled_fonts = 14; + + // PluginRegistration.MasterPages. + repeated MasterPageDefinition master_pages = 15; + + // PluginRegistration.AIActions. + repeated AiAction ai_actions = 16; + + // RBAC roles for the plugin's own Connect services, merged from + // ServiceRegistration (full method name → role, e.g. + // "/symposium.v1.ForumService/CreateThread" → "admin"). + map rbac_method_roles = 17; + + // PluginRegistration.CSSManifest. + CssManifest css_manifest = 18; + + // PluginRegistration.RequiredIconPacks. + repeated string required_icon_packs = 19; + + // PluginRegistration.DirectoryExtensions (static fields only; the + // panel-section / pin-decorator callbacks are counted so the host knows + // how many guest callbacks exist). + DirectoryExtensions directory_extensions = 20; + + // Job types the plugin handles (keys of PluginRegistration.JobHandlers). + // The host dispatches these via the JOB hook. + repeated string job_types = 21; + + // Content types the plugin registered RAG content fetchers for + // (RAGService.RegisterContentFetcher inverts to a manifest declaration; + // the host calls back via the RAG_FETCH hook). + repeated string rag_content_fetcher_types = 22; + + // PluginRegistration.SettingsPanel — Module Federation path of the + // settings panel component ("" when the plugin has none). + string settings_panel = 23; + + // Presence flags for function-valued registration fields that invert to + // hooks at runtime. + bool has_http_handler = 24; // PluginRegistration.HTTPHandler → HANDLE_HTTP + bool has_load_hook = 25; // PluginRegistration.Load → LOAD + bool has_unload_hook = 26; // PluginRegistration.Unload → UNLOAD + bool has_media_hooks = 27; // PluginRegistration.MediaHooks → MEDIA_HOOK + bool has_provisioner = 28; // PluginRegistration.RegisterWithProvisioner + + // Core CMS services the plugin mounts with custom RBAC roles + // (CoreServiceBindings.Bind becomes a static declaration; the host + // constructs and mounts the handlers). + repeated CoreServiceBinding core_service_bindings = 29; + + // data_dir requests a persistent per-plugin /data preopen at load. Its + // source of truth is the plugin.mod `data_dir = true` key, which lives + // outside the guest code, so DESCRIBE cannot populate it: the packer + // (`ninja plugin build`) stamps it into the manifest from plugin.mod so + // the loader reads one source. OFF by default. + bool data_dir = 30; + + // Template tags the plugin provides (blocks.RegisterTag). For each name the + // host registers a pongo2 tag that invokes the guest via HOOK_RENDER_TAG. + // Captured by DESCRIBE from the blocks tag registry (Register-time). + repeated string declared_tags = 31; + // Template filters the plugin provides (blocks.RegisterFilter). For each + // name the host registers a pongo2 filter that invokes the guest via + // HOOK_APPLY_FILTER. Captured by DESCRIBE from the blocks filter registry. + repeated string declared_filters = 32; + + // codeless marks a declarative artifact with NO plugin.wasm (WO-WZ-020): + // the host runs it entirely — block definitions from the artifact's + // blocks/ dir (blocks.yaml manifest-FS layout), seed data from seed/, + // assets/migrations as usual — and never instantiates a guest. A codeless + // manifest MUST NOT declare any computing hook (http handler, jobs, + // load/unload, RAG fetchers, media hooks, tags/filters, provisioner, + // service bindings, directory callbacks); the packer and the host reader + // both reject the combination. Not produced by DESCRIBE (there is no guest + // to describe): `ninja plugin build` synthesizes the manifest from + // plugin.mod + the artifact's declarative files. + bool codeless = 33; +} + +// Dependency mirrors plugin.Dependency. +message Dependency { + string plugin = 1; + string min_version = 2; + bool required = 3; +} + +// BlockMeta mirrors blocks.BlockMeta. Source is omitted: the host assigns it +// from the manifest's plugin name at load time. +message BlockMeta { + string key = 1; + string title = 2; + string description = 3; + // blocks.BlockCategory string ("content", "layout", "navigation", "blog", + // "theme"). + string category = 4; + bool has_internal_slot = 5; + bool hidden = 6; + string editor_js = 7; +} + +// BlockTemplateOverride captures BlockRegistry.RegisterTemplateOverride and +// RegisterTemplateOverrideWithSource registrations. +message BlockTemplateOverride { + string template_key = 1; + string block_key = 2; + // Override source label; empty for plain RegisterTemplateOverride. + string source = 3; +} + +// SystemTemplateMeta mirrors templates.SystemTemplateMeta. +message SystemTemplateMeta { + string key = 1; + string title = 2; + string description = 3; +} + +// PageTemplateMeta mirrors templates.PageTemplateMeta plus the system +// template it was registered under. +message PageTemplateMeta { + string system_key = 1; + string key = 2; + string title = 3; + string description = 4; + repeated string slots = 5; +} + +// AdminPage mirrors plugin.AdminPage. +message AdminPage { + string key = 1; + string title = 2; + string icon = 3; + string route = 4; +} + +// AiAction mirrors plugin.AIAction. +message AiAction { + string key = 1; + string title = 2; + string description = 3; + string default_provider = 4; + string default_model = 5; +} + +// CssManifest mirrors plugin.CSSManifest. +message CssManifest { + map npm_packages = 1; + repeated string css_directives = 2; + string input_css_append = 3; +} + +// DirectoryExtensions mirrors the static fields of plugin.DirectoryExtensions. +message DirectoryExtensions { + repeated string boolean_filter_fields = 1; + repeated string select_filter_fields = 2; + map badge_labels = 3; + // Counts of the callback slices (PanelSections / PinDecorators) so the host + // knows how many guest callbacks the plugin registered. Their invocation + // hook is a runtime-WO concern. + uint32 panel_section_count = 4; + uint32 pin_decorator_count = 5; +} + +// BadgeLabel mirrors the [2]string value of DirectoryExtensions.BadgeLabels. +message BadgeLabel { + string positive = 1; + string negative = 2; +} + +// MasterPageDefinition mirrors plugin.MasterPageDefinition. +message MasterPageDefinition { + string key = 1; + string title = 2; + repeated string page_templates = 3; + repeated MasterPageBlock blocks = 4; +} + +// MasterPageBlock mirrors plugin.MasterPageBlock. +message MasterPageBlock { + string block_key = 1; + string title = 2; + // JSON encoding of the block's content map. + bytes content_json = 3; + optional string html_content = 4; + string slot = 5; + int32 sort_order = 6; +} + +// CoreServiceBinding mirrors a plugin.CoreServiceBindings.Bind call: mount +// the named core-provided Connect service with these RBAC roles. +message CoreServiceBinding { + string service_name = 1; + map method_roles = 2; +} diff --git a/abi/proto/v1/render.proto b/abi/proto/v1/render.proto new file mode 100644 index 0000000..54f62c8 --- /dev/null +++ b/abi/proto/v1/render.proto @@ -0,0 +1,259 @@ +// render.proto — block/template render payloads and the explicit +// render-context envelope (WO-WZ-001). +// +// RenderContext serializes every value blocks currently read from ctx via +// core/blocks/context.go. Function-valued context entries (SlotRenderer, +// MediaResolver, EmbedResolver, the generic Queries value) cannot cross the +// wire; see core/docs/wasm-abi.md for how each one maps. + +syntax = "proto3"; + +package abi.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1"; + +// RenderBlockRequest renders one block (blocks.BlockFunc). +message RenderBlockRequest { + string block_key = 1; + // JSON encoding of the block's content map. + bytes content_json = 2; + RenderContext render_context = 3; +} + +message RenderBlockResponse { + // Final rendered HTML for a plain (non-template) block. Ignored when + // `powered` is set. + string html = 1; + // Set when the block is "powered": instead of final HTML, the block hands + // back a template string + data map (blocks.PoweredBlock) and the HOST + // renders it (pongo2/ninjatpl, host-side) AFTER this RENDER_BLOCK call has + // returned. Because the block-invoke has already returned, the guest + // instance is free, so any plugin tag/filter the template hits (RENDER_TAG / + // APPLY_FILTER) is a fresh invoke — no re-entrancy. A singular message field + // has explicit presence: nil (GetPowered() == nil) means a plain HTML block. + PoweredBlock powered = 2; +} + +// PoweredBlock is a template-backed block result. The host renders `template` +// with `data_json` as the pongo2 data map. See core/docs/wasm-abi.md +// §"Powered blocks (render as a host capability)". +message PoweredBlock { + // ninjatpl/pongo2 template source the host renders host-side. + string template = 1; + // JSON encoding of the template data map (map[string]any). + bytes data_json = 2; +} + +// RenderTemplateRequest renders one template (templates.TemplateFunc). +message RenderTemplateRequest { + string template_key = 1; + // JSON encoding of the template's doc map (the page document). + bytes doc_json = 2; + RenderContext render_context = 3; +} + +message RenderTemplateResponse { + bytes html = 1; +} + +// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG). +// The host wires one pongo2 tag per manifest.declared_tags name; when its +// engine hits that tag while rendering a powered block, it calls back here. +// Re-entrancy-free: the originating RENDER_BLOCK has already returned. +message RenderTagRequest { + string tag_name = 1; + // JSON encoding of the tag's parsed arguments (map[string]any). + bytes args_json = 2; + // The active render context (same envelope RENDER_BLOCK carries), so the + // tag fn can read page/post/request state via blocks.Get*. + RenderContext render_context = 3; +} + +// RenderTagResponse returns the tag's rendered HTML. A non-empty `error` +// carries the tag fn's returned error (distinct from an AbiError, which is +// reserved for transport/dispatch/panic failures). +message RenderTagResponse { + string html = 1; + string error = 2; +} + +// ApplyFilterRequest invokes a plugin-declared template filter +// (HOOK_APPLY_FILTER). The host wires one pongo2 filter per +// manifest.declared_filters name. +message ApplyFilterRequest { + string filter_name = 1; + // The value the filter is applied to (pongo2 passes strings). + string input = 2; + // JSON encoding of the filter's arguments (map[string]any). + bytes args_json = 3; +} + +// ApplyFilterResponse returns the filtered output. A non-empty `error` +// carries the filter fn's returned error. +message ApplyFilterResponse { + string output = 1; + string error = 2; +} + +// RenderContext is the explicit envelope of the context values enumerated +// from core/blocks/context.go. Absent sub-messages mean the corresponding +// ctx value was not set (the getters return nil / zero values). +message RenderContext { + // blocks.GetRequest — subset of *http.Request that render code reads. + RequestInfo request = 1; + // blocks.GetBlockContext — the pongo2 template data struct. + BlockContext block_context = 2; + // blocks.GetTemplateKey. + string template_key = 3; + // blocks.GetCurrentPage. + PageContext page = 4; + // blocks.GetCurrentBlogPost. + PostContext post = 5; + // blocks.GetCurrentAuthor. + AuthorContext author = 6; + // blocks.GetCurrentCategory. + CategoryContext category = 7; + // blocks.GetMasterPage; presence doubles as IsMasterPageContext. + MasterPageContext master_page = 8; + // blocks.GetRequestedPath (404 pages). + string requested_path = 9; + // blocks.GetInjectedSlots (master page rendering). + map injected_slots = 10; + // blocks.IsEditor. + bool is_editor = 11; + // blocks.GetExpectedSlots. + repeated string expected_slots = 12; + // blocks.GetBlockID (UUID; empty for uuid.Nil). + string block_id = 13; + // blocks.GetCurrentPageID (UUID; empty for uuid.Nil). + string current_page_id = 14; + // blocks.GetHumanProofBanner. + HumanProofBanner human_proof_banner = 15; + // blocks.GetDetailRow. + DetailRow detail_row = 16; + // Active theme variables, JSON-encoded. + bytes theme_json = 17; + // Site locale (BCP 47). + string locale = 18; +} + +// RequestInfo carries the request subset render code reads from +// blocks.GetRequest (single-valued header map, matching BlockContext). +message RequestInfo { + string method = 1; + string url = 2; + string path = 3; + string host = 4; + string raw_query = 5; + map headers = 6; + map cookies = 7; + string remote_ip = 8; + string referrer = 9; + string user_agent = 10; +} + +// PageContext mirrors blocks.PageContext. +message PageContext { + string id = 1; // UUID + string slug = 2; + string title = 3; + string post_type = 4; // "page", "post", "master", "system" + string status = 5; // "published", "draft", "scheduled" +} + +// PostContext mirrors blocks.PostContext. +message PostContext { + string id = 1; // UUID + string slug = 2; + string title = 3; + string excerpt = 4; + string featured_image_url = 5; + string author_id = 6; // UUID + google.protobuf.Timestamp published_at = 7; + int32 reading_time = 8; + bool is_featured = 9; +} + +// AuthorContext mirrors blocks.AuthorContext. +message AuthorContext { + string id = 1; // UUID + string name = 2; + string slug = 3; + string bio = 4; + string avatar_url = 5; +} + +// CategoryContext mirrors blocks.CategoryContext. +message CategoryContext { + string id = 1; // UUID + string name = 2; + string slug = 3; +} + +// MasterPageContext mirrors blocks.MasterPageContext. +message MasterPageContext { + string id = 1; // UUID + string slug = 2; + string title = 3; +} + +// HumanProofBanner mirrors blocks.HumanProofBannerData. +message HumanProofBanner { + int32 active_time_minutes = 1; + int32 keystroke_count = 2; + int32 session_count = 3; + string post_slug = 4; +} + +// DetailRow mirrors blocks.DetailRowInfo. +message DetailRow { + string table_id = 1; + string row_id = 2; + // JSON encoding of the row data map. + bytes data_json = 3; +} + +// BlockContext mirrors blocks.BlockContext (the pongo2 data struct) 1:1. +// map[string]any fields travel as JSON bytes. +message BlockContext { + string url = 1; + string path = 2; + string slug = 3; + string page_id = 4; + string page_title = 5; + string template_key = 6; + bool is_editor = 7; + int64 timestamp = 8; + google.protobuf.Timestamp now = 9; + bool is_logged_in = 10; + string user_id = 11; + string user_email = 12; + string user_role = 13; + string method = 14; + string host = 15; + map query = 16; + string referrer = 17; + string user_agent = 18; + string ip = 19; + map cookies = 20; + map headers = 21; + string country = 22; + string city = 23; + string timezone = 24; + bytes current_author_json = 25; + bytes current_post_json = 26; + bytes current_category_json = 27; + bytes site_json = 28; + bool is_public_logged_in = 29; + string public_user_id = 30; + string public_username = 31; + string public_display_name = 32; + bool public_email_verified = 33; + string detail_row_id = 34; + string detail_table_id = 35; + bytes detail_row_data_json = 36; + string blog_index_url = 37; + string category_page_url = 38; +} diff --git a/abi/v1/capability.pb.go b/abi/v1/capability.pb.go new file mode 100644 index 0000000..8baaa3c --- /dev/null +++ b/abi/v1/capability.pb.go @@ -0,0 +1,8094 @@ +// capability.proto — guest→host capability calls (WO-WZ-001). +// +// Each CoreServices interface (core/plugin/deps.go) becomes a host-function +// family; every interface method gets one request/response message pair +// here, mirroring the Go signature 1:1. UUIDs travel as canonical strings; +// map[string]any / []byte-JSON values travel as JSON bytes. +// +// Calls cross via the generic HostCallRequest/HostCallResponse envelope; the +// method string selects the pair (e.g. "content.get_author_profile" → +// ContentGetAuthorProfileRequest/Response). Full method table in +// core/docs/wasm-abi.md. +// +// Not represented here by design: +// - CoreServices.Pool → db.proto (the DB driver messages) +// - CoreServices.Interceptors → host-side only (never crosses) +// - CoreServices.AppURL/MediaPath → LoadRequest.host_config (invoke.proto) +// - CoreServices.CoreServiceBindings → manifest core_service_bindings +// - RAGService.RegisterContentFetcher → manifest rag_content_fetcher_types +// + the RAG_FETCH hook (callback inversion) +// - ai.ToolDefinition.Handler → host→guest tool execution is a runtime-WO +// concern (flagged in core/docs/wasm-abi.md) + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: v1/capability.proto + +package abiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// HostCallRequest is the generic guest→host capability envelope. +type HostCallRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Capability method, "." (e.g. "crypto.encrypt_secret"). + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // Serialized family request message. + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostCallRequest) Reset() { + *x = HostCallRequest{} + mi := &file_v1_capability_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostCallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostCallRequest) ProtoMessage() {} + +func (x *HostCallRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostCallRequest.ProtoReflect.Descriptor instead. +func (*HostCallRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{0} +} + +func (x *HostCallRequest) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *HostCallRequest) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +// HostCallResponse is the generic host→guest capability return envelope. +type HostCallResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Serialized family response message; empty when error is set. + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // Set when the capability call failed; the guest SDK surfaces it as a + // normal Go error. + Error *AbiError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostCallResponse) Reset() { + *x = HostCallResponse{} + mi := &file_v1_capability_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostCallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostCallResponse) ProtoMessage() {} + +func (x *HostCallResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostCallResponse.ProtoReflect.Descriptor instead. +func (*HostCallResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{1} +} + +func (x *HostCallResponse) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *HostCallResponse) GetError() *AbiError { + if x != nil { + return x.Error + } + return nil +} + +type ContentGetAuthorProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGetAuthorProfileRequest) Reset() { + *x = ContentGetAuthorProfileRequest{} + mi := &file_v1_capability_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGetAuthorProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGetAuthorProfileRequest) ProtoMessage() {} + +func (x *ContentGetAuthorProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGetAuthorProfileRequest.ProtoReflect.Descriptor instead. +func (*ContentGetAuthorProfileRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{2} +} + +func (x *ContentGetAuthorProfileRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type ContentGetAuthorProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Author *AuthorProfile `protobuf:"bytes,1,opt,name=author,proto3" json:"author,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGetAuthorProfileResponse) Reset() { + *x = ContentGetAuthorProfileResponse{} + mi := &file_v1_capability_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGetAuthorProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGetAuthorProfileResponse) ProtoMessage() {} + +func (x *ContentGetAuthorProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGetAuthorProfileResponse.ProtoReflect.Descriptor instead. +func (*ContentGetAuthorProfileResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{3} +} + +func (x *ContentGetAuthorProfileResponse) GetAuthor() *AuthorProfile { + if x != nil { + return x.Author + } + return nil +} + +// AuthorProfile mirrors content.AuthorProfile. +type AuthorProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"` + Bio string `protobuf:"bytes,4,opt,name=bio,proto3" json:"bio,omitempty"` + AvatarUrl string `protobuf:"bytes,5,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + Website string `protobuf:"bytes,6,opt,name=website,proto3" json:"website,omitempty"` + SocialLinks map[string]string `protobuf:"bytes,7,rep,name=social_links,json=socialLinks,proto3" json:"social_links,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorProfile) Reset() { + *x = AuthorProfile{} + mi := &file_v1_capability_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorProfile) ProtoMessage() {} + +func (x *AuthorProfile) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorProfile.ProtoReflect.Descriptor instead. +func (*AuthorProfile) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{4} +} + +func (x *AuthorProfile) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AuthorProfile) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AuthorProfile) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *AuthorProfile) GetBio() string { + if x != nil { + return x.Bio + } + return "" +} + +func (x *AuthorProfile) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +func (x *AuthorProfile) GetWebsite() string { + if x != nil { + return x.Website + } + return "" +} + +func (x *AuthorProfile) GetSocialLinks() map[string]string { + if x != nil { + return x.SocialLinks + } + return nil +} + +type ContentGetPageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGetPageRequest) Reset() { + *x = ContentGetPageRequest{} + mi := &file_v1_capability_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGetPageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGetPageRequest) ProtoMessage() {} + +func (x *ContentGetPageRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGetPageRequest.ProtoReflect.Descriptor instead. +func (*ContentGetPageRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{5} +} + +func (x *ContentGetPageRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +type ContentGetPageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Page *PageInfo `protobuf:"bytes,1,opt,name=page,proto3" json:"page,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGetPageResponse) Reset() { + *x = ContentGetPageResponse{} + mi := &file_v1_capability_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGetPageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGetPageResponse) ProtoMessage() {} + +func (x *ContentGetPageResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGetPageResponse.ProtoReflect.Descriptor instead. +func (*ContentGetPageResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{6} +} + +func (x *ContentGetPageResponse) GetPage() *PageInfo { + if x != nil { + return x.Page + } + return nil +} + +// PageInfo mirrors content.PageInfo. +type PageInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PageInfo) Reset() { + *x = PageInfo{} + mi := &file_v1_capability_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PageInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PageInfo) ProtoMessage() {} + +func (x *PageInfo) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PageInfo.ProtoReflect.Descriptor instead. +func (*PageInfo) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{7} +} + +func (x *PageInfo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *PageInfo) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *PageInfo) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +type ContentGetPostRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGetPostRequest) Reset() { + *x = ContentGetPostRequest{} + mi := &file_v1_capability_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGetPostRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGetPostRequest) ProtoMessage() {} + +func (x *ContentGetPostRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGetPostRequest.ProtoReflect.Descriptor instead. +func (*ContentGetPostRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{8} +} + +func (x *ContentGetPostRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +type ContentGetPostResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Post *PostInfo `protobuf:"bytes,1,opt,name=post,proto3" json:"post,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGetPostResponse) Reset() { + *x = ContentGetPostResponse{} + mi := &file_v1_capability_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGetPostResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGetPostResponse) ProtoMessage() {} + +func (x *ContentGetPostResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGetPostResponse.ProtoReflect.Descriptor instead. +func (*ContentGetPostResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{9} +} + +func (x *ContentGetPostResponse) GetPost() *PostInfo { + if x != nil { + return x.Post + } + return nil +} + +// PostInfo mirrors content.PostInfo. +type PostInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + Excerpt string `protobuf:"bytes,4,opt,name=excerpt,proto3" json:"excerpt,omitempty"` + FeaturedImageUrl string `protobuf:"bytes,5,opt,name=featured_image_url,json=featuredImageUrl,proto3" json:"featured_image_url,omitempty"` + AuthorId string `protobuf:"bytes,6,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` // UUID + Body string `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"` // rendered HTML; set by get_post / list_posts(include_body) + AuthorName string `protobuf:"bytes,8,opt,name=author_name,json=authorName,proto3" json:"author_name,omitempty"` + AuthorSlug string `protobuf:"bytes,9,opt,name=author_slug,json=authorSlug,proto3" json:"author_slug,omitempty"` + PublishedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=published_at,json=publishedAt,proto3" json:"published_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostInfo) Reset() { + *x = PostInfo{} + mi := &file_v1_capability_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostInfo) ProtoMessage() {} + +func (x *PostInfo) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PostInfo.ProtoReflect.Descriptor instead. +func (*PostInfo) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{10} +} + +func (x *PostInfo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *PostInfo) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *PostInfo) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *PostInfo) GetExcerpt() string { + if x != nil { + return x.Excerpt + } + return "" +} + +func (x *PostInfo) GetFeaturedImageUrl() string { + if x != nil { + return x.FeaturedImageUrl + } + return "" +} + +func (x *PostInfo) GetAuthorId() string { + if x != nil { + return x.AuthorId + } + return "" +} + +func (x *PostInfo) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +func (x *PostInfo) GetAuthorName() string { + if x != nil { + return x.AuthorName + } + return "" +} + +func (x *PostInfo) GetAuthorSlug() string { + if x != nil { + return x.AuthorSlug + } + return "" +} + +func (x *PostInfo) GetPublishedAt() *timestamppb.Timestamp { + if x != nil { + return x.PublishedAt + } + return nil +} + +type ContentListPostsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` // optional category slug filter ("" = all) + PublishedOnly bool `protobuf:"varint,4,opt,name=published_only,json=publishedOnly,proto3" json:"published_only,omitempty"` + IncludeBody bool `protobuf:"varint,5,opt,name=include_body,json=includeBody,proto3" json:"include_body,omitempty"` + IncludeExcerpt bool `protobuf:"varint,6,opt,name=include_excerpt,json=includeExcerpt,proto3" json:"include_excerpt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentListPostsRequest) Reset() { + *x = ContentListPostsRequest{} + mi := &file_v1_capability_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentListPostsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentListPostsRequest) ProtoMessage() {} + +func (x *ContentListPostsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentListPostsRequest.ProtoReflect.Descriptor instead. +func (*ContentListPostsRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{11} +} + +func (x *ContentListPostsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ContentListPostsRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ContentListPostsRequest) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *ContentListPostsRequest) GetPublishedOnly() bool { + if x != nil { + return x.PublishedOnly + } + return false +} + +func (x *ContentListPostsRequest) GetIncludeBody() bool { + if x != nil { + return x.IncludeBody + } + return false +} + +func (x *ContentListPostsRequest) GetIncludeExcerpt() bool { + if x != nil { + return x.IncludeExcerpt + } + return false +} + +type ContentListPostsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Posts []*PostInfo `protobuf:"bytes,1,rep,name=posts,proto3" json:"posts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentListPostsResponse) Reset() { + *x = ContentListPostsResponse{} + mi := &file_v1_capability_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentListPostsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentListPostsResponse) ProtoMessage() {} + +func (x *ContentListPostsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentListPostsResponse.ProtoReflect.Descriptor instead. +func (*ContentListPostsResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{12} +} + +func (x *ContentListPostsResponse) GetPosts() []*PostInfo { + if x != nil { + return x.Posts + } + return nil +} + +type ContentSlugifyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentSlugifyRequest) Reset() { + *x = ContentSlugifyRequest{} + mi := &file_v1_capability_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentSlugifyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentSlugifyRequest) ProtoMessage() {} + +func (x *ContentSlugifyRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentSlugifyRequest.ProtoReflect.Descriptor instead. +func (*ContentSlugifyRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{13} +} + +func (x *ContentSlugifyRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type ContentSlugifyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentSlugifyResponse) Reset() { + *x = ContentSlugifyResponse{} + mi := &file_v1_capability_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentSlugifyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentSlugifyResponse) ProtoMessage() {} + +func (x *ContentSlugifyResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentSlugifyResponse.ProtoReflect.Descriptor instead. +func (*ContentSlugifyResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{14} +} + +func (x *ContentSlugifyResponse) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +type ContentBlockNoteToHtmlRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // JSON encoding of the BlockNote document map. + DocJson []byte `protobuf:"bytes,1,opt,name=doc_json,json=docJson,proto3" json:"doc_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentBlockNoteToHtmlRequest) Reset() { + *x = ContentBlockNoteToHtmlRequest{} + mi := &file_v1_capability_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentBlockNoteToHtmlRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentBlockNoteToHtmlRequest) ProtoMessage() {} + +func (x *ContentBlockNoteToHtmlRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentBlockNoteToHtmlRequest.ProtoReflect.Descriptor instead. +func (*ContentBlockNoteToHtmlRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{15} +} + +func (x *ContentBlockNoteToHtmlRequest) GetDocJson() []byte { + if x != nil { + return x.DocJson + } + return nil +} + +type ContentBlockNoteToHtmlResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Html string `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentBlockNoteToHtmlResponse) Reset() { + *x = ContentBlockNoteToHtmlResponse{} + mi := &file_v1_capability_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentBlockNoteToHtmlResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentBlockNoteToHtmlResponse) ProtoMessage() {} + +func (x *ContentBlockNoteToHtmlResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentBlockNoteToHtmlResponse.ProtoReflect.Descriptor instead. +func (*ContentBlockNoteToHtmlResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{16} +} + +func (x *ContentBlockNoteToHtmlResponse) GetHtml() string { + if x != nil { + return x.Html + } + return "" +} + +type ContentGenerateExcerptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Html string `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"` + MaxLen int32 `protobuf:"varint,2,opt,name=max_len,json=maxLen,proto3" json:"max_len,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGenerateExcerptRequest) Reset() { + *x = ContentGenerateExcerptRequest{} + mi := &file_v1_capability_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGenerateExcerptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGenerateExcerptRequest) ProtoMessage() {} + +func (x *ContentGenerateExcerptRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGenerateExcerptRequest.ProtoReflect.Descriptor instead. +func (*ContentGenerateExcerptRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{17} +} + +func (x *ContentGenerateExcerptRequest) GetHtml() string { + if x != nil { + return x.Html + } + return "" +} + +func (x *ContentGenerateExcerptRequest) GetMaxLen() int32 { + if x != nil { + return x.MaxLen + } + return 0 +} + +type ContentGenerateExcerptResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Excerpt string `protobuf:"bytes,1,opt,name=excerpt,proto3" json:"excerpt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentGenerateExcerptResponse) Reset() { + *x = ContentGenerateExcerptResponse{} + mi := &file_v1_capability_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentGenerateExcerptResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentGenerateExcerptResponse) ProtoMessage() {} + +func (x *ContentGenerateExcerptResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentGenerateExcerptResponse.ProtoReflect.Descriptor instead. +func (*ContentGenerateExcerptResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{18} +} + +func (x *ContentGenerateExcerptResponse) GetExcerpt() string { + if x != nil { + return x.Excerpt + } + return "" +} + +type ContentStripHtmlRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Html string `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentStripHtmlRequest) Reset() { + *x = ContentStripHtmlRequest{} + mi := &file_v1_capability_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentStripHtmlRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentStripHtmlRequest) ProtoMessage() {} + +func (x *ContentStripHtmlRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentStripHtmlRequest.ProtoReflect.Descriptor instead. +func (*ContentStripHtmlRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{19} +} + +func (x *ContentStripHtmlRequest) GetHtml() string { + if x != nil { + return x.Html + } + return "" +} + +type ContentStripHtmlResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentStripHtmlResponse) Reset() { + *x = ContentStripHtmlResponse{} + mi := &file_v1_capability_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentStripHtmlResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentStripHtmlResponse) ProtoMessage() {} + +func (x *ContentStripHtmlResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentStripHtmlResponse.ProtoReflect.Descriptor instead. +func (*ContentStripHtmlResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{20} +} + +func (x *ContentStripHtmlResponse) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type SettingsGetSiteSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsGetSiteSettingsRequest) Reset() { + *x = SettingsGetSiteSettingsRequest{} + mi := &file_v1_capability_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsGetSiteSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsGetSiteSettingsRequest) ProtoMessage() {} + +func (x *SettingsGetSiteSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsGetSiteSettingsRequest.ProtoReflect.Descriptor instead. +func (*SettingsGetSiteSettingsRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{21} +} + +type SettingsGetSiteSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // JSON encoding of the site settings map. + SettingsJson []byte `protobuf:"bytes,1,opt,name=settings_json,json=settingsJson,proto3" json:"settings_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsGetSiteSettingsResponse) Reset() { + *x = SettingsGetSiteSettingsResponse{} + mi := &file_v1_capability_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsGetSiteSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsGetSiteSettingsResponse) ProtoMessage() {} + +func (x *SettingsGetSiteSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsGetSiteSettingsResponse.ProtoReflect.Descriptor instead. +func (*SettingsGetSiteSettingsResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{22} +} + +func (x *SettingsGetSiteSettingsResponse) GetSettingsJson() []byte { + if x != nil { + return x.SettingsJson + } + return nil +} + +type SettingsGetPluginSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PluginName string `protobuf:"bytes,1,opt,name=plugin_name,json=pluginName,proto3" json:"plugin_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsGetPluginSettingsRequest) Reset() { + *x = SettingsGetPluginSettingsRequest{} + mi := &file_v1_capability_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsGetPluginSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsGetPluginSettingsRequest) ProtoMessage() {} + +func (x *SettingsGetPluginSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsGetPluginSettingsRequest.ProtoReflect.Descriptor instead. +func (*SettingsGetPluginSettingsRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{23} +} + +func (x *SettingsGetPluginSettingsRequest) GetPluginName() string { + if x != nil { + return x.PluginName + } + return "" +} + +type SettingsGetPluginSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // JSON encoding of the plugin settings map. + SettingsJson []byte `protobuf:"bytes,1,opt,name=settings_json,json=settingsJson,proto3" json:"settings_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsGetPluginSettingsResponse) Reset() { + *x = SettingsGetPluginSettingsResponse{} + mi := &file_v1_capability_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsGetPluginSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsGetPluginSettingsResponse) ProtoMessage() {} + +func (x *SettingsGetPluginSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsGetPluginSettingsResponse.ProtoReflect.Descriptor instead. +func (*SettingsGetPluginSettingsResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{24} +} + +func (x *SettingsGetPluginSettingsResponse) GetSettingsJson() []byte { + if x != nil { + return x.SettingsJson + } + return nil +} + +type SettingsUpdateSiteSettingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // JSON encoding of the value. + ValueJson []byte `protobuf:"bytes,2,opt,name=value_json,json=valueJson,proto3" json:"value_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsUpdateSiteSettingRequest) Reset() { + *x = SettingsUpdateSiteSettingRequest{} + mi := &file_v1_capability_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsUpdateSiteSettingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsUpdateSiteSettingRequest) ProtoMessage() {} + +func (x *SettingsUpdateSiteSettingRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsUpdateSiteSettingRequest.ProtoReflect.Descriptor instead. +func (*SettingsUpdateSiteSettingRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{25} +} + +func (x *SettingsUpdateSiteSettingRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *SettingsUpdateSiteSettingRequest) GetValueJson() []byte { + if x != nil { + return x.ValueJson + } + return nil +} + +type SettingsUpdateSiteSettingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsUpdateSiteSettingResponse) Reset() { + *x = SettingsUpdateSiteSettingResponse{} + mi := &file_v1_capability_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsUpdateSiteSettingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsUpdateSiteSettingResponse) ProtoMessage() {} + +func (x *SettingsUpdateSiteSettingResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsUpdateSiteSettingResponse.ProtoReflect.Descriptor instead. +func (*SettingsUpdateSiteSettingResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{26} +} + +type GatingGetSubscriberTierLevelRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatingGetSubscriberTierLevelRequest) Reset() { + *x = GatingGetSubscriberTierLevelRequest{} + mi := &file_v1_capability_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatingGetSubscriberTierLevelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatingGetSubscriberTierLevelRequest) ProtoMessage() {} + +func (x *GatingGetSubscriberTierLevelRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GatingGetSubscriberTierLevelRequest.ProtoReflect.Descriptor instead. +func (*GatingGetSubscriberTierLevelRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{27} +} + +func (x *GatingGetSubscriberTierLevelRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type GatingGetSubscriberTierLevelResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatingGetSubscriberTierLevelResponse) Reset() { + *x = GatingGetSubscriberTierLevelResponse{} + mi := &file_v1_capability_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatingGetSubscriberTierLevelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatingGetSubscriberTierLevelResponse) ProtoMessage() {} + +func (x *GatingGetSubscriberTierLevelResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GatingGetSubscriberTierLevelResponse.ProtoReflect.Descriptor instead. +func (*GatingGetSubscriberTierLevelResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{28} +} + +func (x *GatingGetSubscriberTierLevelResponse) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +type GatingEvaluateAccessRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserTierLevel int32 `protobuf:"varint,1,opt,name=user_tier_level,json=userTierLevel,proto3" json:"user_tier_level,omitempty"` + // Absent rule means "no rule" (access granted). + Rule *AccessRule `protobuf:"bytes,2,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatingEvaluateAccessRequest) Reset() { + *x = GatingEvaluateAccessRequest{} + mi := &file_v1_capability_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatingEvaluateAccessRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatingEvaluateAccessRequest) ProtoMessage() {} + +func (x *GatingEvaluateAccessRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GatingEvaluateAccessRequest.ProtoReflect.Descriptor instead. +func (*GatingEvaluateAccessRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{29} +} + +func (x *GatingEvaluateAccessRequest) GetUserTierLevel() int32 { + if x != nil { + return x.UserTierLevel + } + return 0 +} + +func (x *GatingEvaluateAccessRequest) GetRule() *AccessRule { + if x != nil { + return x.Rule + } + return nil +} + +type GatingEvaluateAccessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *AccessResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatingEvaluateAccessResponse) Reset() { + *x = GatingEvaluateAccessResponse{} + mi := &file_v1_capability_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatingEvaluateAccessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatingEvaluateAccessResponse) ProtoMessage() {} + +func (x *GatingEvaluateAccessResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GatingEvaluateAccessResponse.ProtoReflect.Descriptor instead. +func (*GatingEvaluateAccessResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{30} +} + +func (x *GatingEvaluateAccessResponse) GetResult() *AccessResult { + if x != nil { + return x.Result + } + return nil +} + +// AccessRule mirrors gating.AccessRule. +type AccessRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + MinTierLevel int32 `protobuf:"varint,1,opt,name=min_tier_level,json=minTierLevel,proto3" json:"min_tier_level,omitempty"` + OverrideTierId string `protobuf:"bytes,2,opt,name=override_tier_id,json=overrideTierId,proto3" json:"override_tier_id,omitempty"` + TeaserMode string `protobuf:"bytes,3,opt,name=teaser_mode,json=teaserMode,proto3" json:"teaser_mode,omitempty"` // "hard", "soft", "none" + TeaserPercent int32 `protobuf:"varint,4,opt,name=teaser_percent,json=teaserPercent,proto3" json:"teaser_percent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccessRule) Reset() { + *x = AccessRule{} + mi := &file_v1_capability_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccessRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessRule) ProtoMessage() {} + +func (x *AccessRule) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessRule.ProtoReflect.Descriptor instead. +func (*AccessRule) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{31} +} + +func (x *AccessRule) GetMinTierLevel() int32 { + if x != nil { + return x.MinTierLevel + } + return 0 +} + +func (x *AccessRule) GetOverrideTierId() string { + if x != nil { + return x.OverrideTierId + } + return "" +} + +func (x *AccessRule) GetTeaserMode() string { + if x != nil { + return x.TeaserMode + } + return "" +} + +func (x *AccessRule) GetTeaserPercent() int32 { + if x != nil { + return x.TeaserPercent + } + return 0 +} + +// AccessResult mirrors gating.AccessResult. +type AccessResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + HasAccess bool `protobuf:"varint,1,opt,name=has_access,json=hasAccess,proto3" json:"has_access,omitempty"` + TeaserMode string `protobuf:"bytes,2,opt,name=teaser_mode,json=teaserMode,proto3" json:"teaser_mode,omitempty"` + TeaserPercent int32 `protobuf:"varint,3,opt,name=teaser_percent,json=teaserPercent,proto3" json:"teaser_percent,omitempty"` + RequiredLevel int32 `protobuf:"varint,4,opt,name=required_level,json=requiredLevel,proto3" json:"required_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccessResult) Reset() { + *x = AccessResult{} + mi := &file_v1_capability_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccessResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessResult) ProtoMessage() {} + +func (x *AccessResult) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessResult.ProtoReflect.Descriptor instead. +func (*AccessResult) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{32} +} + +func (x *AccessResult) GetHasAccess() bool { + if x != nil { + return x.HasAccess + } + return false +} + +func (x *AccessResult) GetTeaserMode() string { + if x != nil { + return x.TeaserMode + } + return "" +} + +func (x *AccessResult) GetTeaserPercent() int32 { + if x != nil { + return x.TeaserPercent + } + return 0 +} + +func (x *AccessResult) GetRequiredLevel() int32 { + if x != nil { + return x.RequiredLevel + } + return 0 +} + +type CryptoEncryptSecretRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Plaintext string `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CryptoEncryptSecretRequest) Reset() { + *x = CryptoEncryptSecretRequest{} + mi := &file_v1_capability_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CryptoEncryptSecretRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CryptoEncryptSecretRequest) ProtoMessage() {} + +func (x *CryptoEncryptSecretRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CryptoEncryptSecretRequest.ProtoReflect.Descriptor instead. +func (*CryptoEncryptSecretRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{33} +} + +func (x *CryptoEncryptSecretRequest) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +type CryptoEncryptSecretResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ciphertext string `protobuf:"bytes,1,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CryptoEncryptSecretResponse) Reset() { + *x = CryptoEncryptSecretResponse{} + mi := &file_v1_capability_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CryptoEncryptSecretResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CryptoEncryptSecretResponse) ProtoMessage() {} + +func (x *CryptoEncryptSecretResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CryptoEncryptSecretResponse.ProtoReflect.Descriptor instead. +func (*CryptoEncryptSecretResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{34} +} + +func (x *CryptoEncryptSecretResponse) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +type CryptoDecryptSecretRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ciphertext string `protobuf:"bytes,1,opt,name=ciphertext,proto3" json:"ciphertext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CryptoDecryptSecretRequest) Reset() { + *x = CryptoDecryptSecretRequest{} + mi := &file_v1_capability_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CryptoDecryptSecretRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CryptoDecryptSecretRequest) ProtoMessage() {} + +func (x *CryptoDecryptSecretRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CryptoDecryptSecretRequest.ProtoReflect.Descriptor instead. +func (*CryptoDecryptSecretRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{35} +} + +func (x *CryptoDecryptSecretRequest) GetCiphertext() string { + if x != nil { + return x.Ciphertext + } + return "" +} + +type CryptoDecryptSecretResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Plaintext string `protobuf:"bytes,1,opt,name=plaintext,proto3" json:"plaintext,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CryptoDecryptSecretResponse) Reset() { + *x = CryptoDecryptSecretResponse{} + mi := &file_v1_capability_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CryptoDecryptSecretResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CryptoDecryptSecretResponse) ProtoMessage() {} + +func (x *CryptoDecryptSecretResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CryptoDecryptSecretResponse.ProtoReflect.Descriptor instead. +func (*CryptoDecryptSecretResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{36} +} + +func (x *CryptoDecryptSecretResponse) GetPlaintext() string { + if x != nil { + return x.Plaintext + } + return "" +} + +type MenusGetMenuByNameRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MenusGetMenuByNameRequest) Reset() { + *x = MenusGetMenuByNameRequest{} + mi := &file_v1_capability_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MenusGetMenuByNameRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MenusGetMenuByNameRequest) ProtoMessage() {} + +func (x *MenusGetMenuByNameRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MenusGetMenuByNameRequest.ProtoReflect.Descriptor instead. +func (*MenusGetMenuByNameRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{37} +} + +func (x *MenusGetMenuByNameRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type MenusGetMenuByNameResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Menu *Menu `protobuf:"bytes,1,opt,name=menu,proto3" json:"menu,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MenusGetMenuByNameResponse) Reset() { + *x = MenusGetMenuByNameResponse{} + mi := &file_v1_capability_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MenusGetMenuByNameResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MenusGetMenuByNameResponse) ProtoMessage() {} + +func (x *MenusGetMenuByNameResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MenusGetMenuByNameResponse.ProtoReflect.Descriptor instead. +func (*MenusGetMenuByNameResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{38} +} + +func (x *MenusGetMenuByNameResponse) GetMenu() *Menu { + if x != nil { + return x.Menu + } + return nil +} + +// Menu mirrors menus.Menu. +type Menu struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Menu) Reset() { + *x = Menu{} + mi := &file_v1_capability_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Menu) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Menu) ProtoMessage() {} + +func (x *Menu) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Menu.ProtoReflect.Descriptor instead. +func (*Menu) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{39} +} + +func (x *Menu) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Menu) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type MenusGetMenuItemsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + MenuId string `protobuf:"bytes,1,opt,name=menu_id,json=menuId,proto3" json:"menu_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MenusGetMenuItemsRequest) Reset() { + *x = MenusGetMenuItemsRequest{} + mi := &file_v1_capability_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MenusGetMenuItemsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MenusGetMenuItemsRequest) ProtoMessage() {} + +func (x *MenusGetMenuItemsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MenusGetMenuItemsRequest.ProtoReflect.Descriptor instead. +func (*MenusGetMenuItemsRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{40} +} + +func (x *MenusGetMenuItemsRequest) GetMenuId() string { + if x != nil { + return x.MenuId + } + return "" +} + +type MenusGetMenuItemsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*MenuItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MenusGetMenuItemsResponse) Reset() { + *x = MenusGetMenuItemsResponse{} + mi := &file_v1_capability_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MenusGetMenuItemsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MenusGetMenuItemsResponse) ProtoMessage() {} + +func (x *MenusGetMenuItemsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MenusGetMenuItemsResponse.ProtoReflect.Descriptor instead. +func (*MenusGetMenuItemsResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{41} +} + +func (x *MenusGetMenuItemsResponse) GetItems() []*MenuItem { + if x != nil { + return x.Items + } + return nil +} + +// MenuItem mirrors menus.MenuItem. +type MenuItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + MenuId string `protobuf:"bytes,2,opt,name=menu_id,json=menuId,proto3" json:"menu_id,omitempty"` // UUID + Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` + Url string `protobuf:"bytes,4,opt,name=url,proto3" json:"url,omitempty"` + PageSlug string `protobuf:"bytes,5,opt,name=page_slug,json=pageSlug,proto3" json:"page_slug,omitempty"` + ParentId *string `protobuf:"bytes,6,opt,name=parent_id,json=parentId,proto3,oneof" json:"parent_id,omitempty"` // UUID; unset mirrors nil *uuid.UUID + SortOrder int32 `protobuf:"varint,7,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + OpenInNewTab bool `protobuf:"varint,8,opt,name=open_in_new_tab,json=openInNewTab,proto3" json:"open_in_new_tab,omitempty"` + CssClass string `protobuf:"bytes,9,opt,name=css_class,json=cssClass,proto3" json:"css_class,omitempty"` + ItemType string `protobuf:"bytes,10,opt,name=item_type,json=itemType,proto3" json:"item_type,omitempty"` + Icon string `protobuf:"bytes,11,opt,name=icon,proto3" json:"icon,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MenuItem) Reset() { + *x = MenuItem{} + mi := &file_v1_capability_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MenuItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MenuItem) ProtoMessage() {} + +func (x *MenuItem) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MenuItem.ProtoReflect.Descriptor instead. +func (*MenuItem) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{42} +} + +func (x *MenuItem) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *MenuItem) GetMenuId() string { + if x != nil { + return x.MenuId + } + return "" +} + +func (x *MenuItem) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *MenuItem) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *MenuItem) GetPageSlug() string { + if x != nil { + return x.PageSlug + } + return "" +} + +func (x *MenuItem) GetParentId() string { + if x != nil && x.ParentId != nil { + return *x.ParentId + } + return "" +} + +func (x *MenuItem) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +func (x *MenuItem) GetOpenInNewTab() bool { + if x != nil { + return x.OpenInNewTab + } + return false +} + +func (x *MenuItem) GetCssClass() string { + if x != nil { + return x.CssClass + } + return "" +} + +func (x *MenuItem) GetItemType() string { + if x != nil { + return x.ItemType + } + return "" +} + +func (x *MenuItem) GetIcon() string { + if x != nil { + return x.Icon + } + return "" +} + +type DatasourcesResolveBucketRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BucketId string `protobuf:"bytes,1,opt,name=bucket_id,json=bucketId,proto3" json:"bucket_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DatasourcesResolveBucketRequest) Reset() { + *x = DatasourcesResolveBucketRequest{} + mi := &file_v1_capability_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DatasourcesResolveBucketRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DatasourcesResolveBucketRequest) ProtoMessage() {} + +func (x *DatasourcesResolveBucketRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DatasourcesResolveBucketRequest.ProtoReflect.Descriptor instead. +func (*DatasourcesResolveBucketRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{43} +} + +func (x *DatasourcesResolveBucketRequest) GetBucketId() string { + if x != nil { + return x.BucketId + } + return "" +} + +type DatasourcesResolveBucketResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *DatasourceResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DatasourcesResolveBucketResponse) Reset() { + *x = DatasourcesResolveBucketResponse{} + mi := &file_v1_capability_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DatasourcesResolveBucketResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DatasourcesResolveBucketResponse) ProtoMessage() {} + +func (x *DatasourcesResolveBucketResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DatasourcesResolveBucketResponse.ProtoReflect.Descriptor instead. +func (*DatasourcesResolveBucketResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{44} +} + +func (x *DatasourcesResolveBucketResponse) GetResult() *DatasourceResult { + if x != nil { + return x.Result + } + return nil +} + +type DatasourcesResolveBucketByKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BucketKey string `protobuf:"bytes,1,opt,name=bucket_key,json=bucketKey,proto3" json:"bucket_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DatasourcesResolveBucketByKeyRequest) Reset() { + *x = DatasourcesResolveBucketByKeyRequest{} + mi := &file_v1_capability_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DatasourcesResolveBucketByKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DatasourcesResolveBucketByKeyRequest) ProtoMessage() {} + +func (x *DatasourcesResolveBucketByKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DatasourcesResolveBucketByKeyRequest.ProtoReflect.Descriptor instead. +func (*DatasourcesResolveBucketByKeyRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{45} +} + +func (x *DatasourcesResolveBucketByKeyRequest) GetBucketKey() string { + if x != nil { + return x.BucketKey + } + return "" +} + +type DatasourcesResolveBucketByKeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *DatasourceResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DatasourcesResolveBucketByKeyResponse) Reset() { + *x = DatasourcesResolveBucketByKeyResponse{} + mi := &file_v1_capability_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DatasourcesResolveBucketByKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DatasourcesResolveBucketByKeyResponse) ProtoMessage() {} + +func (x *DatasourcesResolveBucketByKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DatasourcesResolveBucketByKeyResponse.ProtoReflect.Descriptor instead. +func (*DatasourcesResolveBucketByKeyResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{46} +} + +func (x *DatasourcesResolveBucketByKeyResponse) GetResult() *DatasourceResult { + if x != nil { + return x.Result + } + return nil +} + +// DatasourceResult mirrors datasources.Result ([]any / map[string]any as +// JSON bytes). +type DatasourceResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + // JSON array of items. + ItemsJson []byte `protobuf:"bytes,1,opt,name=items_json,json=itemsJson,proto3" json:"items_json,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + // JSON object; empty when absent. + MetaJson []byte `protobuf:"bytes,3,opt,name=meta_json,json=metaJson,proto3" json:"meta_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DatasourceResult) Reset() { + *x = DatasourceResult{} + mi := &file_v1_capability_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DatasourceResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DatasourceResult) ProtoMessage() {} + +func (x *DatasourceResult) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DatasourceResult.ProtoReflect.Descriptor instead. +func (*DatasourceResult) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{47} +} + +func (x *DatasourceResult) GetItemsJson() []byte { + if x != nil { + return x.ItemsJson + } + return nil +} + +func (x *DatasourceResult) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *DatasourceResult) GetMetaJson() []byte { + if x != nil { + return x.MetaJson + } + return nil +} + +type UsersGetByUsernameRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UsersGetByUsernameRequest) Reset() { + *x = UsersGetByUsernameRequest{} + mi := &file_v1_capability_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UsersGetByUsernameRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UsersGetByUsernameRequest) ProtoMessage() {} + +func (x *UsersGetByUsernameRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UsersGetByUsernameRequest.ProtoReflect.Descriptor instead. +func (*UsersGetByUsernameRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{48} +} + +func (x *UsersGetByUsernameRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type UsersGetByUsernameResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *PublicUserProfile `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UsersGetByUsernameResponse) Reset() { + *x = UsersGetByUsernameResponse{} + mi := &file_v1_capability_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UsersGetByUsernameResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UsersGetByUsernameResponse) ProtoMessage() {} + +func (x *UsersGetByUsernameResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UsersGetByUsernameResponse.ProtoReflect.Descriptor instead. +func (*UsersGetByUsernameResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{49} +} + +func (x *UsersGetByUsernameResponse) GetUser() *PublicUserProfile { + if x != nil { + return x.User + } + return nil +} + +type UsersGetByIdRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UsersGetByIdRequest) Reset() { + *x = UsersGetByIdRequest{} + mi := &file_v1_capability_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UsersGetByIdRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UsersGetByIdRequest) ProtoMessage() {} + +func (x *UsersGetByIdRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UsersGetByIdRequest.ProtoReflect.Descriptor instead. +func (*UsersGetByIdRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{50} +} + +func (x *UsersGetByIdRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type UsersGetByIdResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *PublicUserProfile `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UsersGetByIdResponse) Reset() { + *x = UsersGetByIdResponse{} + mi := &file_v1_capability_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UsersGetByIdResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UsersGetByIdResponse) ProtoMessage() {} + +func (x *UsersGetByIdResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UsersGetByIdResponse.ProtoReflect.Descriptor instead. +func (*UsersGetByIdResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{51} +} + +func (x *UsersGetByIdResponse) GetUser() *PublicUserProfile { + if x != nil { + return x.User + } + return nil +} + +// PublicUserProfile mirrors auth.PublicUserProfile. +type PublicUserProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + AvatarUrl string `protobuf:"bytes,5,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + Bio string `protobuf:"bytes,6,opt,name=bio,proto3" json:"bio,omitempty"` + EmailVerified bool `protobuf:"varint,7,opt,name=email_verified,json=emailVerified,proto3" json:"email_verified,omitempty"` + Role string `protobuf:"bytes,8,opt,name=role,proto3" json:"role,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PublicUserProfile) Reset() { + *x = PublicUserProfile{} + mi := &file_v1_capability_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PublicUserProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PublicUserProfile) ProtoMessage() {} + +func (x *PublicUserProfile) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PublicUserProfile.ProtoReflect.Descriptor instead. +func (*PublicUserProfile) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{52} +} + +func (x *PublicUserProfile) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *PublicUserProfile) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *PublicUserProfile) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *PublicUserProfile) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *PublicUserProfile) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +func (x *PublicUserProfile) GetBio() string { + if x != nil { + return x.Bio + } + return "" +} + +func (x *PublicUserProfile) GetEmailVerified() bool { + if x != nil { + return x.EmailVerified + } + return false +} + +func (x *PublicUserProfile) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +type SubscriptionsGetUserTierLevelRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsGetUserTierLevelRequest) Reset() { + *x = SubscriptionsGetUserTierLevelRequest{} + mi := &file_v1_capability_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsGetUserTierLevelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsGetUserTierLevelRequest) ProtoMessage() {} + +func (x *SubscriptionsGetUserTierLevelRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsGetUserTierLevelRequest.ProtoReflect.Descriptor instead. +func (*SubscriptionsGetUserTierLevelRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{53} +} + +func (x *SubscriptionsGetUserTierLevelRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type SubscriptionsGetUserTierLevelResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + TierLevel *TierLevel `protobuf:"bytes,1,opt,name=tier_level,json=tierLevel,proto3" json:"tier_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsGetUserTierLevelResponse) Reset() { + *x = SubscriptionsGetUserTierLevelResponse{} + mi := &file_v1_capability_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsGetUserTierLevelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsGetUserTierLevelResponse) ProtoMessage() {} + +func (x *SubscriptionsGetUserTierLevelResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsGetUserTierLevelResponse.ProtoReflect.Descriptor instead. +func (*SubscriptionsGetUserTierLevelResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{54} +} + +func (x *SubscriptionsGetUserTierLevelResponse) GetTierLevel() *TierLevel { + if x != nil { + return x.TierLevel + } + return nil +} + +// TierLevel mirrors subscriptions.TierLevel. +type TierLevel struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` + // JSON feature payload. + Features []byte `protobuf:"bytes,2,opt,name=features,proto3" json:"features,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TierLevel) Reset() { + *x = TierLevel{} + mi := &file_v1_capability_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TierLevel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TierLevel) ProtoMessage() {} + +func (x *TierLevel) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TierLevel.ProtoReflect.Descriptor instead. +func (*TierLevel) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{55} +} + +func (x *TierLevel) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +func (x *TierLevel) GetFeatures() []byte { + if x != nil { + return x.Features + } + return nil +} + +type SubscriptionsGetTierBySlugRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsGetTierBySlugRequest) Reset() { + *x = SubscriptionsGetTierBySlugRequest{} + mi := &file_v1_capability_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsGetTierBySlugRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsGetTierBySlugRequest) ProtoMessage() {} + +func (x *SubscriptionsGetTierBySlugRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsGetTierBySlugRequest.ProtoReflect.Descriptor instead. +func (*SubscriptionsGetTierBySlugRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{56} +} + +func (x *SubscriptionsGetTierBySlugRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +type SubscriptionsGetTierBySlugResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tier *Tier `protobuf:"bytes,1,opt,name=tier,proto3" json:"tier,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsGetTierBySlugResponse) Reset() { + *x = SubscriptionsGetTierBySlugResponse{} + mi := &file_v1_capability_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsGetTierBySlugResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsGetTierBySlugResponse) ProtoMessage() {} + +func (x *SubscriptionsGetTierBySlugResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsGetTierBySlugResponse.ProtoReflect.Descriptor instead. +func (*SubscriptionsGetTierBySlugResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{57} +} + +func (x *SubscriptionsGetTierBySlugResponse) GetTier() *Tier { + if x != nil { + return x.Tier + } + return nil +} + +// Tier mirrors subscriptions.Tier. +type Tier struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"` + Level int32 `protobuf:"varint,4,opt,name=level,proto3" json:"level,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // JSON feature payload. + Features []byte `protobuf:"bytes,6,opt,name=features,proto3" json:"features,omitempty"` + IsDefault bool `protobuf:"varint,7,opt,name=is_default,json=isDefault,proto3" json:"is_default,omitempty"` + Position int32 `protobuf:"varint,8,opt,name=position,proto3" json:"position,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Tier) Reset() { + *x = Tier{} + mi := &file_v1_capability_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Tier) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Tier) ProtoMessage() {} + +func (x *Tier) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Tier.ProtoReflect.Descriptor instead. +func (*Tier) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{58} +} + +func (x *Tier) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Tier) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Tier) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *Tier) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +func (x *Tier) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Tier) GetFeatures() []byte { + if x != nil { + return x.Features + } + return nil +} + +func (x *Tier) GetIsDefault() bool { + if x != nil { + return x.IsDefault + } + return false +} + +func (x *Tier) GetPosition() int32 { + if x != nil { + return x.Position + } + return 0 +} + +type SubscriptionsListTiersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsListTiersRequest) Reset() { + *x = SubscriptionsListTiersRequest{} + mi := &file_v1_capability_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsListTiersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsListTiersRequest) ProtoMessage() {} + +func (x *SubscriptionsListTiersRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsListTiersRequest.ProtoReflect.Descriptor instead. +func (*SubscriptionsListTiersRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{59} +} + +type SubscriptionsListTiersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tiers []*Tier `protobuf:"bytes,1,rep,name=tiers,proto3" json:"tiers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsListTiersResponse) Reset() { + *x = SubscriptionsListTiersResponse{} + mi := &file_v1_capability_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsListTiersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsListTiersResponse) ProtoMessage() {} + +func (x *SubscriptionsListTiersResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsListTiersResponse.ProtoReflect.Descriptor instead. +func (*SubscriptionsListTiersResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{60} +} + +func (x *SubscriptionsListTiersResponse) GetTiers() []*Tier { + if x != nil { + return x.Tiers + } + return nil +} + +type SubscriptionsListActivePlansRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TierId string `protobuf:"bytes,1,opt,name=tier_id,json=tierId,proto3" json:"tier_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsListActivePlansRequest) Reset() { + *x = SubscriptionsListActivePlansRequest{} + mi := &file_v1_capability_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsListActivePlansRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsListActivePlansRequest) ProtoMessage() {} + +func (x *SubscriptionsListActivePlansRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsListActivePlansRequest.ProtoReflect.Descriptor instead. +func (*SubscriptionsListActivePlansRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{61} +} + +func (x *SubscriptionsListActivePlansRequest) GetTierId() string { + if x != nil { + return x.TierId + } + return "" +} + +type SubscriptionsListActivePlansResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Plans []*Plan `protobuf:"bytes,1,rep,name=plans,proto3" json:"plans,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptionsListActivePlansResponse) Reset() { + *x = SubscriptionsListActivePlansResponse{} + mi := &file_v1_capability_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptionsListActivePlansResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptionsListActivePlansResponse) ProtoMessage() {} + +func (x *SubscriptionsListActivePlansResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptionsListActivePlansResponse.ProtoReflect.Descriptor instead. +func (*SubscriptionsListActivePlansResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{62} +} + +func (x *SubscriptionsListActivePlansResponse) GetPlans() []*Plan { + if x != nil { + return x.Plans + } + return nil +} + +// Plan mirrors subscriptions.Plan. +type Plan struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + TierId string `protobuf:"bytes,2,opt,name=tier_id,json=tierId,proto3" json:"tier_id,omitempty"` // UUID + BillingInterval string `protobuf:"bytes,3,opt,name=billing_interval,json=billingInterval,proto3" json:"billing_interval,omitempty"` + Amount int32 `protobuf:"varint,4,opt,name=amount,proto3" json:"amount,omitempty"` + Currency string `protobuf:"bytes,5,opt,name=currency,proto3" json:"currency,omitempty"` + IsActive bool `protobuf:"varint,6,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Plan) Reset() { + *x = Plan{} + mi := &file_v1_capability_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Plan) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Plan) ProtoMessage() {} + +func (x *Plan) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Plan.ProtoReflect.Descriptor instead. +func (*Plan) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{63} +} + +func (x *Plan) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Plan) GetTierId() string { + if x != nil { + return x.TierId + } + return "" +} + +func (x *Plan) GetBillingInterval() string { + if x != nil { + return x.BillingInterval + } + return "" +} + +func (x *Plan) GetAmount() int32 { + if x != nil { + return x.Amount + } + return 0 +} + +func (x *Plan) GetCurrency() string { + if x != nil { + return x.Currency + } + return "" +} + +func (x *Plan) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *Plan) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +// MediaDepositRequest mirrors plugin.MediaDeposit. +type MediaDepositRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional deterministic media row ID (UUID); empty = host generates one. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"` + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + AltText string `protobuf:"bytes,4,opt,name=alt_text,json=altText,proto3" json:"alt_text,omitempty"` + Folder string `protobuf:"bytes,5,opt,name=folder,proto3" json:"folder,omitempty"` + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MediaDepositRequest) Reset() { + *x = MediaDepositRequest{} + mi := &file_v1_capability_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MediaDepositRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MediaDepositRequest) ProtoMessage() {} + +func (x *MediaDepositRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MediaDepositRequest.ProtoReflect.Descriptor instead. +func (*MediaDepositRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{64} +} + +func (x *MediaDepositRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *MediaDepositRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *MediaDepositRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *MediaDepositRequest) GetAltText() string { + if x != nil { + return x.AltText + } + return "" +} + +func (x *MediaDepositRequest) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +func (x *MediaDepositRequest) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +// MediaDepositResponse mirrors plugin.MediaResult. +type MediaDepositResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + // Ready-to-use reference ("media:"). + Ref string `protobuf:"bytes,2,opt,name=ref,proto3" json:"ref,omitempty"` + Created bool `protobuf:"varint,3,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MediaDepositResponse) Reset() { + *x = MediaDepositResponse{} + mi := &file_v1_capability_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MediaDepositResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MediaDepositResponse) ProtoMessage() {} + +func (x *MediaDepositResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MediaDepositResponse.ProtoReflect.Descriptor instead. +func (*MediaDepositResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{65} +} + +func (x *MediaDepositResponse) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *MediaDepositResponse) GetRef() string { + if x != nil { + return x.Ref + } + return "" +} + +func (x *MediaDepositResponse) GetCreated() bool { + if x != nil { + return x.Created + } + return false +} + +type EmailSendRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + To string `protobuf:"bytes,1,opt,name=to,proto3" json:"to,omitempty"` + Subject string `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"` + Body string `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmailSendRequest) Reset() { + *x = EmailSendRequest{} + mi := &file_v1_capability_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmailSendRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmailSendRequest) ProtoMessage() {} + +func (x *EmailSendRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmailSendRequest.ProtoReflect.Descriptor instead. +func (*EmailSendRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{66} +} + +func (x *EmailSendRequest) GetTo() string { + if x != nil { + return x.To + } + return "" +} + +func (x *EmailSendRequest) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *EmailSendRequest) GetBody() string { + if x != nil { + return x.Body + } + return "" +} + +type EmailSendResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmailSendResponse) Reset() { + *x = EmailSendResponse{} + mi := &file_v1_capability_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmailSendResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmailSendResponse) ProtoMessage() {} + +func (x *EmailSendResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmailSendResponse.ProtoReflect.Descriptor instead. +func (*EmailSendResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{67} +} + +type AiTextCallRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskKey string `protobuf:"bytes,1,opt,name=task_key,json=taskKey,proto3" json:"task_key,omitempty"` + SystemPrompt string `protobuf:"bytes,2,opt,name=system_prompt,json=systemPrompt,proto3" json:"system_prompt,omitempty"` + UserMessage string `protobuf:"bytes,3,opt,name=user_message,json=userMessage,proto3" json:"user_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AiTextCallRequest) Reset() { + *x = AiTextCallRequest{} + mi := &file_v1_capability_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AiTextCallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AiTextCallRequest) ProtoMessage() {} + +func (x *AiTextCallRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AiTextCallRequest.ProtoReflect.Descriptor instead. +func (*AiTextCallRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{68} +} + +func (x *AiTextCallRequest) GetTaskKey() string { + if x != nil { + return x.TaskKey + } + return "" +} + +func (x *AiTextCallRequest) GetSystemPrompt() string { + if x != nil { + return x.SystemPrompt + } + return "" +} + +func (x *AiTextCallRequest) GetUserMessage() string { + if x != nil { + return x.UserMessage + } + return "" +} + +type AiTextCallResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AiTextCallResponse) Reset() { + *x = AiTextCallResponse{} + mi := &file_v1_capability_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AiTextCallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AiTextCallResponse) ProtoMessage() {} + +func (x *AiTextCallResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AiTextCallResponse.ProtoReflect.Descriptor instead. +func (*AiTextCallResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{69} +} + +func (x *AiTextCallResponse) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +// AiToolRegisterRequest mirrors ai.ToolDefinition (minus Handler, which +// stays guest-side; execution direction is a runtime-WO concern). +type AiToolRegisterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // JSON encoding of the parameter schema map. + ParameterSchemaJson []byte `protobuf:"bytes,4,opt,name=parameter_schema_json,json=parameterSchemaJson,proto3" json:"parameter_schema_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AiToolRegisterRequest) Reset() { + *x = AiToolRegisterRequest{} + mi := &file_v1_capability_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AiToolRegisterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AiToolRegisterRequest) ProtoMessage() {} + +func (x *AiToolRegisterRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AiToolRegisterRequest.ProtoReflect.Descriptor instead. +func (*AiToolRegisterRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{70} +} + +func (x *AiToolRegisterRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *AiToolRegisterRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AiToolRegisterRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AiToolRegisterRequest) GetParameterSchemaJson() []byte { + if x != nil { + return x.ParameterSchemaJson + } + return nil +} + +type AiToolRegisterResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AiToolRegisterResponse) Reset() { + *x = AiToolRegisterResponse{} + mi := &file_v1_capability_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AiToolRegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AiToolRegisterResponse) ProtoMessage() {} + +func (x *AiToolRegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AiToolRegisterResponse.ProtoReflect.Descriptor instead. +func (*AiToolRegisterResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{71} +} + +type BridgeRegisterServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PluginName string `protobuf:"bytes,1,opt,name=plugin_name,json=pluginName,proto3" json:"plugin_name,omitempty"` + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeRegisterServiceRequest) Reset() { + *x = BridgeRegisterServiceRequest{} + mi := &file_v1_capability_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeRegisterServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeRegisterServiceRequest) ProtoMessage() {} + +func (x *BridgeRegisterServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[72] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeRegisterServiceRequest.ProtoReflect.Descriptor instead. +func (*BridgeRegisterServiceRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{72} +} + +func (x *BridgeRegisterServiceRequest) GetPluginName() string { + if x != nil { + return x.PluginName + } + return "" +} + +func (x *BridgeRegisterServiceRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +type BridgeRegisterServiceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeRegisterServiceResponse) Reset() { + *x = BridgeRegisterServiceResponse{} + mi := &file_v1_capability_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeRegisterServiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeRegisterServiceResponse) ProtoMessage() {} + +func (x *BridgeRegisterServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[73] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeRegisterServiceResponse.ProtoReflect.Descriptor instead. +func (*BridgeRegisterServiceResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{73} +} + +type BridgeGetServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PluginName string `protobuf:"bytes,1,opt,name=plugin_name,json=pluginName,proto3" json:"plugin_name,omitempty"` + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeGetServiceRequest) Reset() { + *x = BridgeGetServiceRequest{} + mi := &file_v1_capability_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeGetServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeGetServiceRequest) ProtoMessage() {} + +func (x *BridgeGetServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[74] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeGetServiceRequest.ProtoReflect.Descriptor instead. +func (*BridgeGetServiceRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{74} +} + +func (x *BridgeGetServiceRequest) GetPluginName() string { + if x != nil { + return x.PluginName + } + return "" +} + +func (x *BridgeGetServiceRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +type BridgeGetServiceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Available bool `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeGetServiceResponse) Reset() { + *x = BridgeGetServiceResponse{} + mi := &file_v1_capability_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeGetServiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeGetServiceResponse) ProtoMessage() {} + +func (x *BridgeGetServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[75] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeGetServiceResponse.ProtoReflect.Descriptor instead. +func (*BridgeGetServiceResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{75} +} + +func (x *BridgeGetServiceResponse) GetAvailable() bool { + if x != nil { + return x.Available + } + return false +} + +type JobsSubmitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobType string `protobuf:"bytes,1,opt,name=job_type,json=jobType,proto3" json:"job_type,omitempty"` + // JSON job configuration. + ConfigJson []byte `protobuf:"bytes,2,opt,name=config_json,json=configJson,proto3" json:"config_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobsSubmitRequest) Reset() { + *x = JobsSubmitRequest{} + mi := &file_v1_capability_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobsSubmitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobsSubmitRequest) ProtoMessage() {} + +func (x *JobsSubmitRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[76] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobsSubmitRequest.ProtoReflect.Descriptor instead. +func (*JobsSubmitRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{76} +} + +func (x *JobsSubmitRequest) GetJobType() string { + if x != nil { + return x.JobType + } + return "" +} + +func (x *JobsSubmitRequest) GetConfigJson() []byte { + if x != nil { + return x.ConfigJson + } + return nil +} + +type JobsSubmitResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobsSubmitResponse) Reset() { + *x = JobsSubmitResponse{} + mi := &file_v1_capability_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobsSubmitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobsSubmitResponse) ProtoMessage() {} + +func (x *JobsSubmitResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[77] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobsSubmitResponse.ProtoReflect.Descriptor instead. +func (*JobsSubmitResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{77} +} + +type EmbeddingsGenerateEmbeddingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmbeddingsGenerateEmbeddingRequest) Reset() { + *x = EmbeddingsGenerateEmbeddingRequest{} + mi := &file_v1_capability_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmbeddingsGenerateEmbeddingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmbeddingsGenerateEmbeddingRequest) ProtoMessage() {} + +func (x *EmbeddingsGenerateEmbeddingRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[78] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmbeddingsGenerateEmbeddingRequest.ProtoReflect.Descriptor instead. +func (*EmbeddingsGenerateEmbeddingRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{78} +} + +func (x *EmbeddingsGenerateEmbeddingRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type EmbeddingsGenerateEmbeddingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Embedding []float32 `protobuf:"fixed32,1,rep,packed,name=embedding,proto3" json:"embedding,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmbeddingsGenerateEmbeddingResponse) Reset() { + *x = EmbeddingsGenerateEmbeddingResponse{} + mi := &file_v1_capability_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmbeddingsGenerateEmbeddingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmbeddingsGenerateEmbeddingResponse) ProtoMessage() {} + +func (x *EmbeddingsGenerateEmbeddingResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[79] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmbeddingsGenerateEmbeddingResponse.ProtoReflect.Descriptor instead. +func (*EmbeddingsGenerateEmbeddingResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{79} +} + +func (x *EmbeddingsGenerateEmbeddingResponse) GetEmbedding() []float32 { + if x != nil { + return x.Embedding + } + return nil +} + +type EmbeddingsEmbedContentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceType string `protobuf:"bytes,1,opt,name=source_type,json=sourceType,proto3" json:"source_type,omitempty"` + SourceId string `protobuf:"bytes,2,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` // UUID + Text string `protobuf:"bytes,3,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmbeddingsEmbedContentRequest) Reset() { + *x = EmbeddingsEmbedContentRequest{} + mi := &file_v1_capability_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmbeddingsEmbedContentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmbeddingsEmbedContentRequest) ProtoMessage() {} + +func (x *EmbeddingsEmbedContentRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[80] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmbeddingsEmbedContentRequest.ProtoReflect.Descriptor instead. +func (*EmbeddingsEmbedContentRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{80} +} + +func (x *EmbeddingsEmbedContentRequest) GetSourceType() string { + if x != nil { + return x.SourceType + } + return "" +} + +func (x *EmbeddingsEmbedContentRequest) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +func (x *EmbeddingsEmbedContentRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type EmbeddingsEmbedContentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Embedded bool `protobuf:"varint,1,opt,name=embedded,proto3" json:"embedded,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmbeddingsEmbedContentResponse) Reset() { + *x = EmbeddingsEmbedContentResponse{} + mi := &file_v1_capability_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmbeddingsEmbedContentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmbeddingsEmbedContentResponse) ProtoMessage() {} + +func (x *EmbeddingsEmbedContentResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[81] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmbeddingsEmbedContentResponse.ProtoReflect.Descriptor instead. +func (*EmbeddingsEmbedContentResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{81} +} + +func (x *EmbeddingsEmbedContentResponse) GetEmbedded() bool { + if x != nil { + return x.Embedded + } + return false +} + +type EmbeddingsIsAvailableRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmbeddingsIsAvailableRequest) Reset() { + *x = EmbeddingsIsAvailableRequest{} + mi := &file_v1_capability_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmbeddingsIsAvailableRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmbeddingsIsAvailableRequest) ProtoMessage() {} + +func (x *EmbeddingsIsAvailableRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[82] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmbeddingsIsAvailableRequest.ProtoReflect.Descriptor instead. +func (*EmbeddingsIsAvailableRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{82} +} + +type EmbeddingsIsAvailableResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Available bool `protobuf:"varint,1,opt,name=available,proto3" json:"available,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmbeddingsIsAvailableResponse) Reset() { + *x = EmbeddingsIsAvailableResponse{} + mi := &file_v1_capability_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmbeddingsIsAvailableResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmbeddingsIsAvailableResponse) ProtoMessage() {} + +func (x *EmbeddingsIsAvailableResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[83] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmbeddingsIsAvailableResponse.ProtoReflect.Descriptor instead. +func (*EmbeddingsIsAvailableResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{83} +} + +func (x *EmbeddingsIsAvailableResponse) GetAvailable() bool { + if x != nil { + return x.Available + } + return false +} + +type RagQueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RagQueryRequest) Reset() { + *x = RagQueryRequest{} + mi := &file_v1_capability_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RagQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RagQueryRequest) ProtoMessage() {} + +func (x *RagQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[84] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RagQueryRequest.ProtoReflect.Descriptor instead. +func (*RagQueryRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{84} +} + +func (x *RagQueryRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *RagQueryRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type RagQueryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Results []*RagResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RagQueryResponse) Reset() { + *x = RagQueryResponse{} + mi := &file_v1_capability_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RagQueryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RagQueryResponse) ProtoMessage() {} + +func (x *RagQueryResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[85] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RagQueryResponse.ProtoReflect.Descriptor instead. +func (*RagQueryResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{85} +} + +func (x *RagQueryResponse) GetResults() []*RagResult { + if x != nil { + return x.Results + } + return nil +} + +// RagResult mirrors plugin.RAGResult. +type RagResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + Score float64 `protobuf:"fixed64,2,opt,name=score,proto3" json:"score,omitempty"` + Metadata map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RagResult) Reset() { + *x = RagResult{} + mi := &file_v1_capability_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RagResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RagResult) ProtoMessage() {} + +func (x *RagResult) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[86] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RagResult.ProtoReflect.Descriptor instead. +func (*RagResult) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{86} +} + +func (x *RagResult) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *RagResult) GetScore() float64 { + if x != nil { + return x.Score + } + return 0 +} + +func (x *RagResult) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type RagOnContentChangedRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContentType string `protobuf:"bytes,1,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + ContentId string `protobuf:"bytes,2,opt,name=content_id,json=contentId,proto3" json:"content_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RagOnContentChangedRequest) Reset() { + *x = RagOnContentChangedRequest{} + mi := &file_v1_capability_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RagOnContentChangedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RagOnContentChangedRequest) ProtoMessage() {} + +func (x *RagOnContentChangedRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[87] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RagOnContentChangedRequest.ProtoReflect.Descriptor instead. +func (*RagOnContentChangedRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{87} +} + +func (x *RagOnContentChangedRequest) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *RagOnContentChangedRequest) GetContentId() string { + if x != nil { + return x.ContentId + } + return "" +} + +type RagOnContentChangedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RagOnContentChangedResponse) Reset() { + *x = RagOnContentChangedResponse{} + mi := &file_v1_capability_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RagOnContentChangedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RagOnContentChangedResponse) ProtoMessage() {} + +func (x *RagOnContentChangedResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[88] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RagOnContentChangedResponse.ProtoReflect.Descriptor instead. +func (*RagOnContentChangedResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{88} +} + +// ReviewsSubmitReviewRequest mirrors plugin.SubmitReviewParams. +type ReviewsSubmitReviewRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TableId string `protobuf:"bytes,1,opt,name=table_id,json=tableId,proto3" json:"table_id,omitempty"` // UUID + RowId string `protobuf:"bytes,2,opt,name=row_id,json=rowId,proto3" json:"row_id,omitempty"` // UUID + OverallRating int32 `protobuf:"varint,3,opt,name=overall_rating,json=overallRating,proto3" json:"overall_rating,omitempty"` + ReviewText string `protobuf:"bytes,4,opt,name=review_text,json=reviewText,proto3" json:"review_text,omitempty"` + // JSON encoding of the per-criterion ratings map. + RatingsJson []byte `protobuf:"bytes,5,opt,name=ratings_json,json=ratingsJson,proto3" json:"ratings_json,omitempty"` + Photos []string `protobuf:"bytes,6,rep,name=photos,proto3" json:"photos,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReviewsSubmitReviewRequest) Reset() { + *x = ReviewsSubmitReviewRequest{} + mi := &file_v1_capability_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReviewsSubmitReviewRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReviewsSubmitReviewRequest) ProtoMessage() {} + +func (x *ReviewsSubmitReviewRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[89] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReviewsSubmitReviewRequest.ProtoReflect.Descriptor instead. +func (*ReviewsSubmitReviewRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{89} +} + +func (x *ReviewsSubmitReviewRequest) GetTableId() string { + if x != nil { + return x.TableId + } + return "" +} + +func (x *ReviewsSubmitReviewRequest) GetRowId() string { + if x != nil { + return x.RowId + } + return "" +} + +func (x *ReviewsSubmitReviewRequest) GetOverallRating() int32 { + if x != nil { + return x.OverallRating + } + return 0 +} + +func (x *ReviewsSubmitReviewRequest) GetReviewText() string { + if x != nil { + return x.ReviewText + } + return "" +} + +func (x *ReviewsSubmitReviewRequest) GetRatingsJson() []byte { + if x != nil { + return x.RatingsJson + } + return nil +} + +func (x *ReviewsSubmitReviewRequest) GetPhotos() []string { + if x != nil { + return x.Photos + } + return nil +} + +type ReviewsSubmitReviewResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReviewId string `protobuf:"bytes,1,opt,name=review_id,json=reviewId,proto3" json:"review_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReviewsSubmitReviewResponse) Reset() { + *x = ReviewsSubmitReviewResponse{} + mi := &file_v1_capability_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReviewsSubmitReviewResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReviewsSubmitReviewResponse) ProtoMessage() {} + +func (x *ReviewsSubmitReviewResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[90] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReviewsSubmitReviewResponse.ProtoReflect.Descriptor instead. +func (*ReviewsSubmitReviewResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{90} +} + +func (x *ReviewsSubmitReviewResponse) GetReviewId() string { + if x != nil { + return x.ReviewId + } + return "" +} + +type BadgesRefreshBadgesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TableId string `protobuf:"bytes,1,opt,name=table_id,json=tableId,proto3" json:"table_id,omitempty"` // UUID + RowId string `protobuf:"bytes,2,opt,name=row_id,json=rowId,proto3" json:"row_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BadgesRefreshBadgesRequest) Reset() { + *x = BadgesRefreshBadgesRequest{} + mi := &file_v1_capability_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BadgesRefreshBadgesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BadgesRefreshBadgesRequest) ProtoMessage() {} + +func (x *BadgesRefreshBadgesRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[91] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BadgesRefreshBadgesRequest.ProtoReflect.Descriptor instead. +func (*BadgesRefreshBadgesRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{91} +} + +func (x *BadgesRefreshBadgesRequest) GetTableId() string { + if x != nil { + return x.TableId + } + return "" +} + +func (x *BadgesRefreshBadgesRequest) GetRowId() string { + if x != nil { + return x.RowId + } + return "" +} + +type BadgesRefreshBadgesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BadgesRefreshBadgesResponse) Reset() { + *x = BadgesRefreshBadgesResponse{} + mi := &file_v1_capability_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BadgesRefreshBadgesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BadgesRefreshBadgesResponse) ProtoMessage() {} + +func (x *BadgesRefreshBadgesResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[92] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BadgesRefreshBadgesResponse.ProtoReflect.Descriptor instead. +func (*BadgesRefreshBadgesResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{92} +} + +type ContentCreatePageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + ParentSlug string `protobuf:"bytes,2,opt,name=parent_slug,json=parentSlug,proto3" json:"parent_slug,omitempty"` // "" = root + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + TemplateKey string `protobuf:"bytes,4,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` + MasterPageKey string `protobuf:"bytes,5,opt,name=master_page_key,json=masterPageKey,proto3" json:"master_page_key,omitempty"` // "" = none + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentCreatePageRequest) Reset() { + *x = ContentCreatePageRequest{} + mi := &file_v1_capability_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentCreatePageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentCreatePageRequest) ProtoMessage() {} + +func (x *ContentCreatePageRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[93] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentCreatePageRequest.ProtoReflect.Descriptor instead. +func (*ContentCreatePageRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{93} +} + +func (x *ContentCreatePageRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *ContentCreatePageRequest) GetParentSlug() string { + if x != nil { + return x.ParentSlug + } + return "" +} + +func (x *ContentCreatePageRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ContentCreatePageRequest) GetTemplateKey() string { + if x != nil { + return x.TemplateKey + } + return "" +} + +func (x *ContentCreatePageRequest) GetMasterPageKey() string { + if x != nil { + return x.MasterPageKey + } + return "" +} + +type ContentCreatePageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + PageId string `protobuf:"bytes,1,opt,name=page_id,json=pageId,proto3" json:"page_id,omitempty"` // UUID + // False when a page with this slug already existed (the existing ID is + // returned and nothing is modified). + Created bool `protobuf:"varint,2,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentCreatePageResponse) Reset() { + *x = ContentCreatePageResponse{} + mi := &file_v1_capability_proto_msgTypes[94] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentCreatePageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentCreatePageResponse) ProtoMessage() {} + +func (x *ContentCreatePageResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[94] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentCreatePageResponse.ProtoReflect.Descriptor instead. +func (*ContentCreatePageResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{94} +} + +func (x *ContentCreatePageResponse) GetPageId() string { + if x != nil { + return x.PageId + } + return "" +} + +func (x *ContentCreatePageResponse) GetCreated() bool { + if x != nil { + return x.Created + } + return false +} + +// PageBlock is one block instance placed on a page (mirrors +// plugin.PageBlockConfig / MasterPageBlock). +type PageBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + BlockKey string `protobuf:"bytes,1,opt,name=block_key,json=blockKey,proto3" json:"block_key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // JSON encoding of the block's content map. + ContentJson []byte `protobuf:"bytes,3,opt,name=content_json,json=contentJson,proto3" json:"content_json,omitempty"` + HtmlContent *string `protobuf:"bytes,4,opt,name=html_content,json=htmlContent,proto3,oneof" json:"html_content,omitempty"` + Slot string `protobuf:"bytes,5,opt,name=slot,proto3" json:"slot,omitempty"` + SortOrder int32 `protobuf:"varint,6,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PageBlock) Reset() { + *x = PageBlock{} + mi := &file_v1_capability_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PageBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PageBlock) ProtoMessage() {} + +func (x *PageBlock) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[95] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PageBlock.ProtoReflect.Descriptor instead. +func (*PageBlock) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{95} +} + +func (x *PageBlock) GetBlockKey() string { + if x != nil { + return x.BlockKey + } + return "" +} + +func (x *PageBlock) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *PageBlock) GetContentJson() []byte { + if x != nil { + return x.ContentJson + } + return nil +} + +func (x *PageBlock) GetHtmlContent() string { + if x != nil && x.HtmlContent != nil { + return *x.HtmlContent + } + return "" +} + +func (x *PageBlock) GetSlot() string { + if x != nil { + return x.Slot + } + return "" +} + +func (x *PageBlock) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +type ContentSetPageBlocksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PageId string `protobuf:"bytes,1,opt,name=page_id,json=pageId,proto3" json:"page_id,omitempty"` // UUID + // Full replacement set for the page's draft document blocks. + Blocks []*PageBlock `protobuf:"bytes,2,rep,name=blocks,proto3" json:"blocks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentSetPageBlocksRequest) Reset() { + *x = ContentSetPageBlocksRequest{} + mi := &file_v1_capability_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentSetPageBlocksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentSetPageBlocksRequest) ProtoMessage() {} + +func (x *ContentSetPageBlocksRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[96] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentSetPageBlocksRequest.ProtoReflect.Descriptor instead. +func (*ContentSetPageBlocksRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{96} +} + +func (x *ContentSetPageBlocksRequest) GetPageId() string { + if x != nil { + return x.PageId + } + return "" +} + +func (x *ContentSetPageBlocksRequest) GetBlocks() []*PageBlock { + if x != nil { + return x.Blocks + } + return nil +} + +type ContentSetPageBlocksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentSetPageBlocksResponse) Reset() { + *x = ContentSetPageBlocksResponse{} + mi := &file_v1_capability_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentSetPageBlocksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentSetPageBlocksResponse) ProtoMessage() {} + +func (x *ContentSetPageBlocksResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[97] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentSetPageBlocksResponse.ProtoReflect.Descriptor instead. +func (*ContentSetPageBlocksResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{97} +} + +type ContentPublishPageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PageId string `protobuf:"bytes,1,opt,name=page_id,json=pageId,proto3" json:"page_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentPublishPageRequest) Reset() { + *x = ContentPublishPageRequest{} + mi := &file_v1_capability_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentPublishPageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentPublishPageRequest) ProtoMessage() {} + +func (x *ContentPublishPageRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[98] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentPublishPageRequest.ProtoReflect.Descriptor instead. +func (*ContentPublishPageRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{98} +} + +func (x *ContentPublishPageRequest) GetPageId() string { + if x != nil { + return x.PageId + } + return "" +} + +type ContentPublishPageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentPublishPageResponse) Reset() { + *x = ContentPublishPageResponse{} + mi := &file_v1_capability_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentPublishPageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentPublishPageResponse) ProtoMessage() {} + +func (x *ContentPublishPageResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[99] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentPublishPageResponse.ProtoReflect.Descriptor instead. +func (*ContentPublishPageResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{99} +} + +// ContentSetPageSeoRequest mirrors the CMS UpdatePageSEO surface. Unset +// optionals leave the existing value untouched. +type ContentSetPageSeoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PageId string `protobuf:"bytes,1,opt,name=page_id,json=pageId,proto3" json:"page_id,omitempty"` // UUID + MetaTitle *string `protobuf:"bytes,2,opt,name=meta_title,json=metaTitle,proto3,oneof" json:"meta_title,omitempty"` + MetaDescription *string `protobuf:"bytes,3,opt,name=meta_description,json=metaDescription,proto3,oneof" json:"meta_description,omitempty"` + OgTitle *string `protobuf:"bytes,4,opt,name=og_title,json=ogTitle,proto3,oneof" json:"og_title,omitempty"` + OgDescription *string `protobuf:"bytes,5,opt,name=og_description,json=ogDescription,proto3,oneof" json:"og_description,omitempty"` + OgImage *string `protobuf:"bytes,6,opt,name=og_image,json=ogImage,proto3,oneof" json:"og_image,omitempty"` + FocusKeyphrase *string `protobuf:"bytes,7,opt,name=focus_keyphrase,json=focusKeyphrase,proto3,oneof" json:"focus_keyphrase,omitempty"` + CanonicalUrl *string `protobuf:"bytes,8,opt,name=canonical_url,json=canonicalUrl,proto3,oneof" json:"canonical_url,omitempty"` + RobotsDirective *string `protobuf:"bytes,9,opt,name=robots_directive,json=robotsDirective,proto3,oneof" json:"robots_directive,omitempty"` + TwitterTitle *string `protobuf:"bytes,10,opt,name=twitter_title,json=twitterTitle,proto3,oneof" json:"twitter_title,omitempty"` + TwitterDescription *string `protobuf:"bytes,11,opt,name=twitter_description,json=twitterDescription,proto3,oneof" json:"twitter_description,omitempty"` + TwitterImage *string `protobuf:"bytes,12,opt,name=twitter_image,json=twitterImage,proto3,oneof" json:"twitter_image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentSetPageSeoRequest) Reset() { + *x = ContentSetPageSeoRequest{} + mi := &file_v1_capability_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentSetPageSeoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentSetPageSeoRequest) ProtoMessage() {} + +func (x *ContentSetPageSeoRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[100] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentSetPageSeoRequest.ProtoReflect.Descriptor instead. +func (*ContentSetPageSeoRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{100} +} + +func (x *ContentSetPageSeoRequest) GetPageId() string { + if x != nil { + return x.PageId + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetMetaTitle() string { + if x != nil && x.MetaTitle != nil { + return *x.MetaTitle + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetMetaDescription() string { + if x != nil && x.MetaDescription != nil { + return *x.MetaDescription + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetOgTitle() string { + if x != nil && x.OgTitle != nil { + return *x.OgTitle + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetOgDescription() string { + if x != nil && x.OgDescription != nil { + return *x.OgDescription + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetOgImage() string { + if x != nil && x.OgImage != nil { + return *x.OgImage + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetFocusKeyphrase() string { + if x != nil && x.FocusKeyphrase != nil { + return *x.FocusKeyphrase + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetCanonicalUrl() string { + if x != nil && x.CanonicalUrl != nil { + return *x.CanonicalUrl + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetRobotsDirective() string { + if x != nil && x.RobotsDirective != nil { + return *x.RobotsDirective + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetTwitterTitle() string { + if x != nil && x.TwitterTitle != nil { + return *x.TwitterTitle + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetTwitterDescription() string { + if x != nil && x.TwitterDescription != nil { + return *x.TwitterDescription + } + return "" +} + +func (x *ContentSetPageSeoRequest) GetTwitterImage() string { + if x != nil && x.TwitterImage != nil { + return *x.TwitterImage + } + return "" +} + +type ContentSetPageSeoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentSetPageSeoResponse) Reset() { + *x = ContentSetPageSeoResponse{} + mi := &file_v1_capability_proto_msgTypes[101] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentSetPageSeoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentSetPageSeoResponse) ProtoMessage() {} + +func (x *ContentSetPageSeoResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[101] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentSetPageSeoResponse.ProtoReflect.Descriptor instead. +func (*ContentSetPageSeoResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{101} +} + +// ContentUpsertPostRequest creates or updates a blog post by slug (posts live +// in the dedicated blog_posts table). +type ContentUpsertPostRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // JSON encoding of the BlockNote document. + DocumentJson []byte `protobuf:"bytes,3,opt,name=document_json,json=documentJson,proto3" json:"document_json,omitempty"` + Excerpt *string `protobuf:"bytes,4,opt,name=excerpt,proto3,oneof" json:"excerpt,omitempty"` + AuthorProfileId *string `protobuf:"bytes,5,opt,name=author_profile_id,json=authorProfileId,proto3,oneof" json:"author_profile_id,omitempty"` // UUID + FeaturedImageId *string `protobuf:"bytes,6,opt,name=featured_image_id,json=featuredImageId,proto3,oneof" json:"featured_image_id,omitempty"` // UUID (e.g. from media.deposit) + // Publish immediately after the upsert. + Publish bool `protobuf:"varint,7,opt,name=publish,proto3" json:"publish,omitempty"` + IsFeatured bool `protobuf:"varint,8,opt,name=is_featured,json=isFeatured,proto3" json:"is_featured,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentUpsertPostRequest) Reset() { + *x = ContentUpsertPostRequest{} + mi := &file_v1_capability_proto_msgTypes[102] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentUpsertPostRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentUpsertPostRequest) ProtoMessage() {} + +func (x *ContentUpsertPostRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[102] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentUpsertPostRequest.ProtoReflect.Descriptor instead. +func (*ContentUpsertPostRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{102} +} + +func (x *ContentUpsertPostRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *ContentUpsertPostRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ContentUpsertPostRequest) GetDocumentJson() []byte { + if x != nil { + return x.DocumentJson + } + return nil +} + +func (x *ContentUpsertPostRequest) GetExcerpt() string { + if x != nil && x.Excerpt != nil { + return *x.Excerpt + } + return "" +} + +func (x *ContentUpsertPostRequest) GetAuthorProfileId() string { + if x != nil && x.AuthorProfileId != nil { + return *x.AuthorProfileId + } + return "" +} + +func (x *ContentUpsertPostRequest) GetFeaturedImageId() string { + if x != nil && x.FeaturedImageId != nil { + return *x.FeaturedImageId + } + return "" +} + +func (x *ContentUpsertPostRequest) GetPublish() bool { + if x != nil { + return x.Publish + } + return false +} + +func (x *ContentUpsertPostRequest) GetIsFeatured() bool { + if x != nil { + return x.IsFeatured + } + return false +} + +type ContentUpsertPostResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + PostId string `protobuf:"bytes,1,opt,name=post_id,json=postId,proto3" json:"post_id,omitempty"` // UUID + Created bool `protobuf:"varint,2,opt,name=created,proto3" json:"created,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContentUpsertPostResponse) Reset() { + *x = ContentUpsertPostResponse{} + mi := &file_v1_capability_proto_msgTypes[103] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContentUpsertPostResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContentUpsertPostResponse) ProtoMessage() {} + +func (x *ContentUpsertPostResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[103] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ContentUpsertPostResponse.ProtoReflect.Descriptor instead. +func (*ContentUpsertPostResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{103} +} + +func (x *ContentUpsertPostResponse) GetPostId() string { + if x != nil { + return x.PostId + } + return "" +} + +func (x *ContentUpsertPostResponse) GetCreated() bool { + if x != nil { + return x.Created + } + return false +} + +type ProvisionerEnsureDataTableRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // JSON schema of the table (json.RawMessage in DataTableConfig). + SchemaJson []byte `protobuf:"bytes,4,opt,name=schema_json,json=schemaJson,proto3" json:"schema_json,omitempty"` + PrimaryKey string `protobuf:"bytes,5,opt,name=primary_key,json=primaryKey,proto3" json:"primary_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureDataTableRequest) Reset() { + *x = ProvisionerEnsureDataTableRequest{} + mi := &file_v1_capability_proto_msgTypes[104] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureDataTableRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureDataTableRequest) ProtoMessage() {} + +func (x *ProvisionerEnsureDataTableRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[104] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureDataTableRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureDataTableRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{104} +} + +func (x *ProvisionerEnsureDataTableRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *ProvisionerEnsureDataTableRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ProvisionerEnsureDataTableRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProvisionerEnsureDataTableRequest) GetSchemaJson() []byte { + if x != nil { + return x.SchemaJson + } + return nil +} + +func (x *ProvisionerEnsureDataTableRequest) GetPrimaryKey() string { + if x != nil { + return x.PrimaryKey + } + return "" +} + +type ProvisionerEnsureDataTableResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureDataTableResponse) Reset() { + *x = ProvisionerEnsureDataTableResponse{} + mi := &file_v1_capability_proto_msgTypes[105] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureDataTableResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureDataTableResponse) ProtoMessage() {} + +func (x *ProvisionerEnsureDataTableResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[105] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureDataTableResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureDataTableResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{105} +} + +type ProvisionerMergeSiteSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // JSON encoding of the defaults map; existing keys win. + DefaultsJson []byte `protobuf:"bytes,1,opt,name=defaults_json,json=defaultsJson,proto3" json:"defaults_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerMergeSiteSettingsRequest) Reset() { + *x = ProvisionerMergeSiteSettingsRequest{} + mi := &file_v1_capability_proto_msgTypes[106] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerMergeSiteSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerMergeSiteSettingsRequest) ProtoMessage() {} + +func (x *ProvisionerMergeSiteSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[106] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerMergeSiteSettingsRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerMergeSiteSettingsRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{106} +} + +func (x *ProvisionerMergeSiteSettingsRequest) GetDefaultsJson() []byte { + if x != nil { + return x.DefaultsJson + } + return nil +} + +type ProvisionerMergeSiteSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerMergeSiteSettingsResponse) Reset() { + *x = ProvisionerMergeSiteSettingsResponse{} + mi := &file_v1_capability_proto_msgTypes[107] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerMergeSiteSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerMergeSiteSettingsResponse) ProtoMessage() {} + +func (x *ProvisionerMergeSiteSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[107] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerMergeSiteSettingsResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerMergeSiteSettingsResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{107} +} + +type ProvisionerEnsureSettingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // JSON encoding of the default value. + DefaultValueJson []byte `protobuf:"bytes,2,opt,name=default_value_json,json=defaultValueJson,proto3" json:"default_value_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureSettingRequest) Reset() { + *x = ProvisionerEnsureSettingRequest{} + mi := &file_v1_capability_proto_msgTypes[108] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureSettingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureSettingRequest) ProtoMessage() {} + +func (x *ProvisionerEnsureSettingRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[108] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureSettingRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureSettingRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{108} +} + +func (x *ProvisionerEnsureSettingRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *ProvisionerEnsureSettingRequest) GetDefaultValueJson() []byte { + if x != nil { + return x.DefaultValueJson + } + return nil +} + +type ProvisionerEnsureSettingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureSettingResponse) Reset() { + *x = ProvisionerEnsureSettingResponse{} + mi := &file_v1_capability_proto_msgTypes[109] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureSettingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureSettingResponse) ProtoMessage() {} + +func (x *ProvisionerEnsureSettingResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[109] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureSettingResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureSettingResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{109} +} + +// PageSeed mirrors plugin.PageConfig. +type PageSeed struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + ParentSlug string `protobuf:"bytes,2,opt,name=parent_slug,json=parentSlug,proto3" json:"parent_slug,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + TemplateKey string `protobuf:"bytes,4,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` + Blocks []*PageBlock `protobuf:"bytes,5,rep,name=blocks,proto3" json:"blocks,omitempty"` + DetailSourceType string `protobuf:"bytes,6,opt,name=detail_source_type,json=detailSourceType,proto3" json:"detail_source_type,omitempty"` + DetailSourceKey string `protobuf:"bytes,7,opt,name=detail_source_key,json=detailSourceKey,proto3" json:"detail_source_key,omitempty"` + DetailSlugField string `protobuf:"bytes,8,opt,name=detail_slug_field,json=detailSlugField,proto3" json:"detail_slug_field,omitempty"` + ReconcileBlocks bool `protobuf:"varint,9,opt,name=reconcile_blocks,json=reconcileBlocks,proto3" json:"reconcile_blocks,omitempty"` + ReconcileTemplate bool `protobuf:"varint,10,opt,name=reconcile_template,json=reconcileTemplate,proto3" json:"reconcile_template,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PageSeed) Reset() { + *x = PageSeed{} + mi := &file_v1_capability_proto_msgTypes[110] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PageSeed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PageSeed) ProtoMessage() {} + +func (x *PageSeed) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[110] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PageSeed.ProtoReflect.Descriptor instead. +func (*PageSeed) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{110} +} + +func (x *PageSeed) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *PageSeed) GetParentSlug() string { + if x != nil { + return x.ParentSlug + } + return "" +} + +func (x *PageSeed) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *PageSeed) GetTemplateKey() string { + if x != nil { + return x.TemplateKey + } + return "" +} + +func (x *PageSeed) GetBlocks() []*PageBlock { + if x != nil { + return x.Blocks + } + return nil +} + +func (x *PageSeed) GetDetailSourceType() string { + if x != nil { + return x.DetailSourceType + } + return "" +} + +func (x *PageSeed) GetDetailSourceKey() string { + if x != nil { + return x.DetailSourceKey + } + return "" +} + +func (x *PageSeed) GetDetailSlugField() string { + if x != nil { + return x.DetailSlugField + } + return "" +} + +func (x *PageSeed) GetReconcileBlocks() bool { + if x != nil { + return x.ReconcileBlocks + } + return false +} + +func (x *PageSeed) GetReconcileTemplate() bool { + if x != nil { + return x.ReconcileTemplate + } + return false +} + +type ProvisionerEnsurePageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Page *PageSeed `protobuf:"bytes,1,opt,name=page,proto3" json:"page,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsurePageRequest) Reset() { + *x = ProvisionerEnsurePageRequest{} + mi := &file_v1_capability_proto_msgTypes[111] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsurePageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsurePageRequest) ProtoMessage() {} + +func (x *ProvisionerEnsurePageRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[111] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsurePageRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsurePageRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{111} +} + +func (x *ProvisionerEnsurePageRequest) GetPage() *PageSeed { + if x != nil { + return x.Page + } + return nil +} + +type ProvisionerEnsurePageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsurePageResponse) Reset() { + *x = ProvisionerEnsurePageResponse{} + mi := &file_v1_capability_proto_msgTypes[112] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsurePageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsurePageResponse) ProtoMessage() {} + +func (x *ProvisionerEnsurePageResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[112] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsurePageResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsurePageResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{112} +} + +type ProvisionerOverrideSiteSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // JSON encoding of the overrides map; plugin values win. + OverridesJson []byte `protobuf:"bytes,1,opt,name=overrides_json,json=overridesJson,proto3" json:"overrides_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerOverrideSiteSettingsRequest) Reset() { + *x = ProvisionerOverrideSiteSettingsRequest{} + mi := &file_v1_capability_proto_msgTypes[113] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerOverrideSiteSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerOverrideSiteSettingsRequest) ProtoMessage() {} + +func (x *ProvisionerOverrideSiteSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[113] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerOverrideSiteSettingsRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerOverrideSiteSettingsRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{113} +} + +func (x *ProvisionerOverrideSiteSettingsRequest) GetOverridesJson() []byte { + if x != nil { + return x.OverridesJson + } + return nil +} + +type ProvisionerOverrideSiteSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerOverrideSiteSettingsResponse) Reset() { + *x = ProvisionerOverrideSiteSettingsResponse{} + mi := &file_v1_capability_proto_msgTypes[114] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerOverrideSiteSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerOverrideSiteSettingsResponse) ProtoMessage() {} + +func (x *ProvisionerOverrideSiteSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[114] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerOverrideSiteSettingsResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerOverrideSiteSettingsResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{114} +} + +// ProvisionerEnsureMenuItemRequest mirrors plugin.MenuItemConfig under a +// named menu. +type ProvisionerEnsureMenuItemRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + MenuName string `protobuf:"bytes,1,opt,name=menu_name,json=menuName,proto3" json:"menu_name,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + PageSlug string `protobuf:"bytes,4,opt,name=page_slug,json=pageSlug,proto3" json:"page_slug,omitempty"` + SortOrder int32 `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureMenuItemRequest) Reset() { + *x = ProvisionerEnsureMenuItemRequest{} + mi := &file_v1_capability_proto_msgTypes[115] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureMenuItemRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureMenuItemRequest) ProtoMessage() {} + +func (x *ProvisionerEnsureMenuItemRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[115] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureMenuItemRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureMenuItemRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{115} +} + +func (x *ProvisionerEnsureMenuItemRequest) GetMenuName() string { + if x != nil { + return x.MenuName + } + return "" +} + +func (x *ProvisionerEnsureMenuItemRequest) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *ProvisionerEnsureMenuItemRequest) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *ProvisionerEnsureMenuItemRequest) GetPageSlug() string { + if x != nil { + return x.PageSlug + } + return "" +} + +func (x *ProvisionerEnsureMenuItemRequest) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +type ProvisionerEnsureMenuItemResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureMenuItemResponse) Reset() { + *x = ProvisionerEnsureMenuItemResponse{} + mi := &file_v1_capability_proto_msgTypes[116] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureMenuItemResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureMenuItemResponse) ProtoMessage() {} + +func (x *ProvisionerEnsureMenuItemResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[116] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureMenuItemResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureMenuItemResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{116} +} + +// ProvisionerRegisterEmbeddingConfigRequest mirrors plugin.EmbeddingConfigDef. +type ProvisionerRegisterEmbeddingConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TableKey string `protobuf:"bytes,1,opt,name=table_key,json=tableKey,proto3" json:"table_key,omitempty"` + TextTemplate string `protobuf:"bytes,2,opt,name=text_template,json=textTemplate,proto3" json:"text_template,omitempty"` + Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerRegisterEmbeddingConfigRequest) Reset() { + *x = ProvisionerRegisterEmbeddingConfigRequest{} + mi := &file_v1_capability_proto_msgTypes[117] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerRegisterEmbeddingConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerRegisterEmbeddingConfigRequest) ProtoMessage() {} + +func (x *ProvisionerRegisterEmbeddingConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[117] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerRegisterEmbeddingConfigRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerRegisterEmbeddingConfigRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{117} +} + +func (x *ProvisionerRegisterEmbeddingConfigRequest) GetTableKey() string { + if x != nil { + return x.TableKey + } + return "" +} + +func (x *ProvisionerRegisterEmbeddingConfigRequest) GetTextTemplate() string { + if x != nil { + return x.TextTemplate + } + return "" +} + +func (x *ProvisionerRegisterEmbeddingConfigRequest) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type ProvisionerRegisterEmbeddingConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerRegisterEmbeddingConfigResponse) Reset() { + *x = ProvisionerRegisterEmbeddingConfigResponse{} + mi := &file_v1_capability_proto_msgTypes[118] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerRegisterEmbeddingConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerRegisterEmbeddingConfigResponse) ProtoMessage() {} + +func (x *ProvisionerRegisterEmbeddingConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[118] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerRegisterEmbeddingConfigResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerRegisterEmbeddingConfigResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{118} +} + +// ProvisionerEnsureEmbedRequest mirrors plugin.EmbedConfig minus RenderFunc +// (function values cannot cross; the Template renders host-side). +type ProvisionerEnsureEmbedRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Icon string `protobuf:"bytes,4,opt,name=icon,proto3" json:"icon,omitempty"` + LabelField string `protobuf:"bytes,5,opt,name=label_field,json=labelField,proto3" json:"label_field,omitempty"` + Template string `protobuf:"bytes,6,opt,name=template,proto3" json:"template,omitempty"` + DataSourceType string `protobuf:"bytes,7,opt,name=data_source_type,json=dataSourceType,proto3" json:"data_source_type,omitempty"` // EmbedDataSource.Type + DataSourceTableKey string `protobuf:"bytes,8,opt,name=data_source_table_key,json=dataSourceTableKey,proto3" json:"data_source_table_key,omitempty"` // EmbedDataSource.TableKey + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureEmbedRequest) Reset() { + *x = ProvisionerEnsureEmbedRequest{} + mi := &file_v1_capability_proto_msgTypes[119] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureEmbedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureEmbedRequest) ProtoMessage() {} + +func (x *ProvisionerEnsureEmbedRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[119] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureEmbedRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureEmbedRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{119} +} + +func (x *ProvisionerEnsureEmbedRequest) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *ProvisionerEnsureEmbedRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ProvisionerEnsureEmbedRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProvisionerEnsureEmbedRequest) GetIcon() string { + if x != nil { + return x.Icon + } + return "" +} + +func (x *ProvisionerEnsureEmbedRequest) GetLabelField() string { + if x != nil { + return x.LabelField + } + return "" +} + +func (x *ProvisionerEnsureEmbedRequest) GetTemplate() string { + if x != nil { + return x.Template + } + return "" +} + +func (x *ProvisionerEnsureEmbedRequest) GetDataSourceType() string { + if x != nil { + return x.DataSourceType + } + return "" +} + +func (x *ProvisionerEnsureEmbedRequest) GetDataSourceTableKey() string { + if x != nil { + return x.DataSourceTableKey + } + return "" +} + +type ProvisionerEnsureEmbedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureEmbedResponse) Reset() { + *x = ProvisionerEnsureEmbedResponse{} + mi := &file_v1_capability_proto_msgTypes[120] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureEmbedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureEmbedResponse) ProtoMessage() {} + +func (x *ProvisionerEnsureEmbedResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[120] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureEmbedResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureEmbedResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{120} +} + +// ProvisionerEnsureJobScheduleRequest mirrors plugin.JobScheduleConfig. +type ProvisionerEnsureJobScheduleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobType string `protobuf:"bytes,1,opt,name=job_type,json=jobType,proto3" json:"job_type,omitempty"` + CronExpression string `protobuf:"bytes,2,opt,name=cron_expression,json=cronExpression,proto3" json:"cron_expression,omitempty"` + // JSON job configuration. + ConfigJson []byte `protobuf:"bytes,3,opt,name=config_json,json=configJson,proto3" json:"config_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureJobScheduleRequest) Reset() { + *x = ProvisionerEnsureJobScheduleRequest{} + mi := &file_v1_capability_proto_msgTypes[121] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureJobScheduleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureJobScheduleRequest) ProtoMessage() {} + +func (x *ProvisionerEnsureJobScheduleRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[121] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureJobScheduleRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureJobScheduleRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{121} +} + +func (x *ProvisionerEnsureJobScheduleRequest) GetJobType() string { + if x != nil { + return x.JobType + } + return "" +} + +func (x *ProvisionerEnsureJobScheduleRequest) GetCronExpression() string { + if x != nil { + return x.CronExpression + } + return "" +} + +func (x *ProvisionerEnsureJobScheduleRequest) GetConfigJson() []byte { + if x != nil { + return x.ConfigJson + } + return nil +} + +type ProvisionerEnsureJobScheduleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureJobScheduleResponse) Reset() { + *x = ProvisionerEnsureJobScheduleResponse{} + mi := &file_v1_capability_proto_msgTypes[122] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureJobScheduleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureJobScheduleResponse) ProtoMessage() {} + +func (x *ProvisionerEnsureJobScheduleResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[122] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureJobScheduleResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureJobScheduleResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{122} +} + +type ProvisionerUpdateDataTableRowFieldRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RowId string `protobuf:"bytes,1,opt,name=row_id,json=rowId,proto3" json:"row_id,omitempty"` // UUID + FieldKey string `protobuf:"bytes,2,opt,name=field_key,json=fieldKey,proto3" json:"field_key,omitempty"` + // JSON encoding of the value. + ValueJson []byte `protobuf:"bytes,3,opt,name=value_json,json=valueJson,proto3" json:"value_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerUpdateDataTableRowFieldRequest) Reset() { + *x = ProvisionerUpdateDataTableRowFieldRequest{} + mi := &file_v1_capability_proto_msgTypes[123] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerUpdateDataTableRowFieldRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerUpdateDataTableRowFieldRequest) ProtoMessage() {} + +func (x *ProvisionerUpdateDataTableRowFieldRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[123] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerUpdateDataTableRowFieldRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerUpdateDataTableRowFieldRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{123} +} + +func (x *ProvisionerUpdateDataTableRowFieldRequest) GetRowId() string { + if x != nil { + return x.RowId + } + return "" +} + +func (x *ProvisionerUpdateDataTableRowFieldRequest) GetFieldKey() string { + if x != nil { + return x.FieldKey + } + return "" +} + +func (x *ProvisionerUpdateDataTableRowFieldRequest) GetValueJson() []byte { + if x != nil { + return x.ValueJson + } + return nil +} + +type ProvisionerUpdateDataTableRowFieldResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerUpdateDataTableRowFieldResponse) Reset() { + *x = ProvisionerUpdateDataTableRowFieldResponse{} + mi := &file_v1_capability_proto_msgTypes[124] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerUpdateDataTableRowFieldResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerUpdateDataTableRowFieldResponse) ProtoMessage() {} + +func (x *ProvisionerUpdateDataTableRowFieldResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[124] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerUpdateDataTableRowFieldResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerUpdateDataTableRowFieldResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{124} +} + +type ProvisionerDisableOrphanedJobSchedulesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RegisteredTypes []string `protobuf:"bytes,1,rep,name=registered_types,json=registeredTypes,proto3" json:"registered_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerDisableOrphanedJobSchedulesRequest) Reset() { + *x = ProvisionerDisableOrphanedJobSchedulesRequest{} + mi := &file_v1_capability_proto_msgTypes[125] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerDisableOrphanedJobSchedulesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerDisableOrphanedJobSchedulesRequest) ProtoMessage() {} + +func (x *ProvisionerDisableOrphanedJobSchedulesRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[125] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerDisableOrphanedJobSchedulesRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerDisableOrphanedJobSchedulesRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{125} +} + +func (x *ProvisionerDisableOrphanedJobSchedulesRequest) GetRegisteredTypes() []string { + if x != nil { + return x.RegisteredTypes + } + return nil +} + +type ProvisionerDisableOrphanedJobSchedulesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerDisableOrphanedJobSchedulesResponse) Reset() { + *x = ProvisionerDisableOrphanedJobSchedulesResponse{} + mi := &file_v1_capability_proto_msgTypes[126] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerDisableOrphanedJobSchedulesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerDisableOrphanedJobSchedulesResponse) ProtoMessage() {} + +func (x *ProvisionerDisableOrphanedJobSchedulesResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[126] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerDisableOrphanedJobSchedulesResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerDisableOrphanedJobSchedulesResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{126} +} + +type ProvisionerEnsurePluginRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsurePluginRequest) Reset() { + *x = ProvisionerEnsurePluginRequest{} + mi := &file_v1_capability_proto_msgTypes[127] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsurePluginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsurePluginRequest) ProtoMessage() {} + +func (x *ProvisionerEnsurePluginRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[127] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsurePluginRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsurePluginRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{127} +} + +func (x *ProvisionerEnsurePluginRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type ProvisionerEnsurePluginResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsurePluginResponse) Reset() { + *x = ProvisionerEnsurePluginResponse{} + mi := &file_v1_capability_proto_msgTypes[128] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsurePluginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsurePluginResponse) ProtoMessage() {} + +func (x *ProvisionerEnsurePluginResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[128] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsurePluginResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsurePluginResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{128} +} + +// ProvisionerEnsureCustomColorRequest mirrors plugin.CustomColorConfig. +type ProvisionerEnsureCustomColorRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + LightValue string `protobuf:"bytes,2,opt,name=light_value,json=lightValue,proto3" json:"light_value,omitempty"` + DarkValue string `protobuf:"bytes,3,opt,name=dark_value,json=darkValue,proto3" json:"dark_value,omitempty"` + Source string `protobuf:"bytes,4,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureCustomColorRequest) Reset() { + *x = ProvisionerEnsureCustomColorRequest{} + mi := &file_v1_capability_proto_msgTypes[129] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureCustomColorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureCustomColorRequest) ProtoMessage() {} + +func (x *ProvisionerEnsureCustomColorRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[129] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureCustomColorRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureCustomColorRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{129} +} + +func (x *ProvisionerEnsureCustomColorRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ProvisionerEnsureCustomColorRequest) GetLightValue() string { + if x != nil { + return x.LightValue + } + return "" +} + +func (x *ProvisionerEnsureCustomColorRequest) GetDarkValue() string { + if x != nil { + return x.DarkValue + } + return "" +} + +func (x *ProvisionerEnsureCustomColorRequest) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +type ProvisionerEnsureCustomColorResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureCustomColorResponse) Reset() { + *x = ProvisionerEnsureCustomColorResponse{} + mi := &file_v1_capability_proto_msgTypes[130] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureCustomColorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureCustomColorResponse) ProtoMessage() {} + +func (x *ProvisionerEnsureCustomColorResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[130] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureCustomColorResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureCustomColorResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{130} +} + +// ProvisionerEnsureMediaRequest mirrors plugin.MediaDeposit with a REQUIRED +// deterministic id (the template-referable key). Idempotent: an existing id +// is a no-op; changed bytes warn rather than overwrite. +type ProvisionerEnsureMediaRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID, required + Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"` + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + AltText string `protobuf:"bytes,4,opt,name=alt_text,json=altText,proto3" json:"alt_text,omitempty"` + Folder string `protobuf:"bytes,5,opt,name=folder,proto3" json:"folder,omitempty"` + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureMediaRequest) Reset() { + *x = ProvisionerEnsureMediaRequest{} + mi := &file_v1_capability_proto_msgTypes[131] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureMediaRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureMediaRequest) ProtoMessage() {} + +func (x *ProvisionerEnsureMediaRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[131] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureMediaRequest.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureMediaRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{131} +} + +func (x *ProvisionerEnsureMediaRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ProvisionerEnsureMediaRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *ProvisionerEnsureMediaRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *ProvisionerEnsureMediaRequest) GetAltText() string { + if x != nil { + return x.AltText + } + return "" +} + +func (x *ProvisionerEnsureMediaRequest) GetFolder() string { + if x != nil { + return x.Folder + } + return "" +} + +func (x *ProvisionerEnsureMediaRequest) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +type ProvisionerEnsureMediaResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProvisionerEnsureMediaResponse) Reset() { + *x = ProvisionerEnsureMediaResponse{} + mi := &file_v1_capability_proto_msgTypes[132] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProvisionerEnsureMediaResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerEnsureMediaResponse) ProtoMessage() {} + +func (x *ProvisionerEnsureMediaResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[132] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerEnsureMediaResponse.ProtoReflect.Descriptor instead. +func (*ProvisionerEnsureMediaResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{132} +} + +// SettingsUpdatePluginSettingsRequest replaces the calling plugin's own +// settings map. The host binds the target plugin from the caller's identity; +// plugin_name is advisory and MUST match it (PERMISSION_DENIED otherwise). +type SettingsUpdatePluginSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PluginName string `protobuf:"bytes,1,opt,name=plugin_name,json=pluginName,proto3" json:"plugin_name,omitempty"` + // JSON encoding of the full settings map. + SettingsJson []byte `protobuf:"bytes,2,opt,name=settings_json,json=settingsJson,proto3" json:"settings_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsUpdatePluginSettingsRequest) Reset() { + *x = SettingsUpdatePluginSettingsRequest{} + mi := &file_v1_capability_proto_msgTypes[133] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsUpdatePluginSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsUpdatePluginSettingsRequest) ProtoMessage() {} + +func (x *SettingsUpdatePluginSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[133] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsUpdatePluginSettingsRequest.ProtoReflect.Descriptor instead. +func (*SettingsUpdatePluginSettingsRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{133} +} + +func (x *SettingsUpdatePluginSettingsRequest) GetPluginName() string { + if x != nil { + return x.PluginName + } + return "" +} + +func (x *SettingsUpdatePluginSettingsRequest) GetSettingsJson() []byte { + if x != nil { + return x.SettingsJson + } + return nil +} + +type SettingsUpdatePluginSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SettingsUpdatePluginSettingsResponse) Reset() { + *x = SettingsUpdatePluginSettingsResponse{} + mi := &file_v1_capability_proto_msgTypes[134] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SettingsUpdatePluginSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SettingsUpdatePluginSettingsResponse) ProtoMessage() {} + +func (x *SettingsUpdatePluginSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[134] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SettingsUpdatePluginSettingsResponse.ProtoReflect.Descriptor instead. +func (*SettingsUpdatePluginSettingsResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{134} +} + +// JobsProgressRequest reports progress for the CURRENTLY EXECUTING job. The +// host correlates it to the job via the invoking HOOK_JOB call's context, so +// it is only meaningful while a JOB hook is on the stack; outside one it is +// accepted and discarded. +type JobsProgressRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Current int32 `protobuf:"varint,1,opt,name=current,proto3" json:"current,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobsProgressRequest) Reset() { + *x = JobsProgressRequest{} + mi := &file_v1_capability_proto_msgTypes[135] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobsProgressRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobsProgressRequest) ProtoMessage() {} + +func (x *JobsProgressRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[135] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobsProgressRequest.ProtoReflect.Descriptor instead. +func (*JobsProgressRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{135} +} + +func (x *JobsProgressRequest) GetCurrent() int32 { + if x != nil { + return x.Current + } + return 0 +} + +func (x *JobsProgressRequest) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *JobsProgressRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type JobsProgressResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobsProgressResponse) Reset() { + *x = JobsProgressResponse{} + mi := &file_v1_capability_proto_msgTypes[136] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobsProgressResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobsProgressResponse) ProtoMessage() {} + +func (x *JobsProgressResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[136] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobsProgressResponse.ProtoReflect.Descriptor instead. +func (*JobsProgressResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{136} +} + +// BridgeInvokeRequest calls a method on another plugin's registered bridge +// service. The provider answers via HOOK_BRIDGE_CALL (wasm) or an in-process +// plugin.BridgeInvokable (bundled). Payloads are opaque bytes — the two +// plugins agree on the encoding (JSON by convention). +type BridgeInvokeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PluginName string `protobuf:"bytes,1,opt,name=plugin_name,json=pluginName,proto3" json:"plugin_name,omitempty"` + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + Payload []byte `protobuf:"bytes,4,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeInvokeRequest) Reset() { + *x = BridgeInvokeRequest{} + mi := &file_v1_capability_proto_msgTypes[137] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeInvokeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeInvokeRequest) ProtoMessage() {} + +func (x *BridgeInvokeRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[137] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeInvokeRequest.ProtoReflect.Descriptor instead. +func (*BridgeInvokeRequest) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{137} +} + +func (x *BridgeInvokeRequest) GetPluginName() string { + if x != nil { + return x.PluginName + } + return "" +} + +func (x *BridgeInvokeRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *BridgeInvokeRequest) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *BridgeInvokeRequest) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +type BridgeInvokeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeInvokeResponse) Reset() { + *x = BridgeInvokeResponse{} + mi := &file_v1_capability_proto_msgTypes[138] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeInvokeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeInvokeResponse) ProtoMessage() {} + +func (x *BridgeInvokeResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_capability_proto_msgTypes[138] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeInvokeResponse.ProtoReflect.Descriptor instead. +func (*BridgeInvokeResponse) Descriptor() ([]byte, []int) { + return file_v1_capability_proto_rawDescGZIP(), []int{138} +} + +func (x *BridgeInvokeResponse) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +var File_v1_capability_proto protoreflect.FileDescriptor + +const file_v1_capability_proto_rawDesc = "" + + "\n" + + "\x13v1/capability.proto\x12\x06abi.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x0fv1/invoke.proto\"C\n" + + "\x0fHostCallRequest\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x18\n" + + "\apayload\x18\x02 \x01(\fR\apayload\"T\n" + + "\x10HostCallResponse\x12\x18\n" + + "\apayload\x18\x01 \x01(\fR\apayload\x12&\n" + + "\x05error\x18\x02 \x01(\v2\x10.abi.v1.AbiErrorR\x05error\"0\n" + + "\x1eContentGetAuthorProfileRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"P\n" + + "\x1fContentGetAuthorProfileResponse\x12-\n" + + "\x06author\x18\x01 \x01(\v2\x15.abi.v1.AuthorProfileR\x06author\"\x9d\x02\n" + + "\rAuthorProfile\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04slug\x18\x03 \x01(\tR\x04slug\x12\x10\n" + + "\x03bio\x18\x04 \x01(\tR\x03bio\x12\x1d\n" + + "\n" + + "avatar_url\x18\x05 \x01(\tR\tavatarUrl\x12\x18\n" + + "\awebsite\x18\x06 \x01(\tR\awebsite\x12I\n" + + "\fsocial_links\x18\a \x03(\v2&.abi.v1.AuthorProfile.SocialLinksEntryR\vsocialLinks\x1a>\n" + + "\x10SocialLinksEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"+\n" + + "\x15ContentGetPageRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\">\n" + + "\x16ContentGetPageResponse\x12$\n" + + "\x04page\x18\x01 \x01(\v2\x10.abi.v1.PageInfoR\x04page\"D\n" + + "\bPageInfo\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\"+\n" + + "\x15ContentGetPostRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\">\n" + + "\x16ContentGetPostResponse\x12$\n" + + "\x04post\x18\x01 \x01(\v2\x10.abi.v1.PostInfoR\x04post\"\xbe\x02\n" + + "\bPostInfo\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\x12\x18\n" + + "\aexcerpt\x18\x04 \x01(\tR\aexcerpt\x12,\n" + + "\x12featured_image_url\x18\x05 \x01(\tR\x10featuredImageUrl\x12\x1b\n" + + "\tauthor_id\x18\x06 \x01(\tR\bauthorId\x12\x12\n" + + "\x04body\x18\a \x01(\tR\x04body\x12\x1f\n" + + "\vauthor_name\x18\b \x01(\tR\n" + + "authorName\x12\x1f\n" + + "\vauthor_slug\x18\t \x01(\tR\n" + + "authorSlug\x12=\n" + + "\fpublished_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\vpublishedAt\"\xd6\x01\n" + + "\x17ContentListPostsRequest\x12\x14\n" + + "\x05limit\x18\x01 \x01(\x05R\x05limit\x12\x16\n" + + "\x06offset\x18\x02 \x01(\x05R\x06offset\x12\x1a\n" + + "\bcategory\x18\x03 \x01(\tR\bcategory\x12%\n" + + "\x0epublished_only\x18\x04 \x01(\bR\rpublishedOnly\x12!\n" + + "\finclude_body\x18\x05 \x01(\bR\vincludeBody\x12'\n" + + "\x0finclude_excerpt\x18\x06 \x01(\bR\x0eincludeExcerpt\"B\n" + + "\x18ContentListPostsResponse\x12&\n" + + "\x05posts\x18\x01 \x03(\v2\x10.abi.v1.PostInfoR\x05posts\"+\n" + + "\x15ContentSlugifyRequest\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\",\n" + + "\x16ContentSlugifyResponse\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\":\n" + + "\x1dContentBlockNoteToHtmlRequest\x12\x19\n" + + "\bdoc_json\x18\x01 \x01(\fR\adocJson\"4\n" + + "\x1eContentBlockNoteToHtmlResponse\x12\x12\n" + + "\x04html\x18\x01 \x01(\tR\x04html\"L\n" + + "\x1dContentGenerateExcerptRequest\x12\x12\n" + + "\x04html\x18\x01 \x01(\tR\x04html\x12\x17\n" + + "\amax_len\x18\x02 \x01(\x05R\x06maxLen\":\n" + + "\x1eContentGenerateExcerptResponse\x12\x18\n" + + "\aexcerpt\x18\x01 \x01(\tR\aexcerpt\"-\n" + + "\x17ContentStripHtmlRequest\x12\x12\n" + + "\x04html\x18\x01 \x01(\tR\x04html\".\n" + + "\x18ContentStripHtmlResponse\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\" \n" + + "\x1eSettingsGetSiteSettingsRequest\"F\n" + + "\x1fSettingsGetSiteSettingsResponse\x12#\n" + + "\rsettings_json\x18\x01 \x01(\fR\fsettingsJson\"C\n" + + " SettingsGetPluginSettingsRequest\x12\x1f\n" + + "\vplugin_name\x18\x01 \x01(\tR\n" + + "pluginName\"H\n" + + "!SettingsGetPluginSettingsResponse\x12#\n" + + "\rsettings_json\x18\x01 \x01(\fR\fsettingsJson\"S\n" + + " SettingsUpdateSiteSettingRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x1d\n" + + "\n" + + "value_json\x18\x02 \x01(\fR\tvalueJson\"#\n" + + "!SettingsUpdateSiteSettingResponse\">\n" + + "#GatingGetSubscriberTierLevelRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\"<\n" + + "$GatingGetSubscriberTierLevelResponse\x12\x14\n" + + "\x05level\x18\x01 \x01(\x05R\x05level\"m\n" + + "\x1bGatingEvaluateAccessRequest\x12&\n" + + "\x0fuser_tier_level\x18\x01 \x01(\x05R\ruserTierLevel\x12&\n" + + "\x04rule\x18\x02 \x01(\v2\x12.abi.v1.AccessRuleR\x04rule\"L\n" + + "\x1cGatingEvaluateAccessResponse\x12,\n" + + "\x06result\x18\x01 \x01(\v2\x14.abi.v1.AccessResultR\x06result\"\xa4\x01\n" + + "\n" + + "AccessRule\x12$\n" + + "\x0emin_tier_level\x18\x01 \x01(\x05R\fminTierLevel\x12(\n" + + "\x10override_tier_id\x18\x02 \x01(\tR\x0eoverrideTierId\x12\x1f\n" + + "\vteaser_mode\x18\x03 \x01(\tR\n" + + "teaserMode\x12%\n" + + "\x0eteaser_percent\x18\x04 \x01(\x05R\rteaserPercent\"\x9c\x01\n" + + "\fAccessResult\x12\x1d\n" + + "\n" + + "has_access\x18\x01 \x01(\bR\thasAccess\x12\x1f\n" + + "\vteaser_mode\x18\x02 \x01(\tR\n" + + "teaserMode\x12%\n" + + "\x0eteaser_percent\x18\x03 \x01(\x05R\rteaserPercent\x12%\n" + + "\x0erequired_level\x18\x04 \x01(\x05R\rrequiredLevel\":\n" + + "\x1aCryptoEncryptSecretRequest\x12\x1c\n" + + "\tplaintext\x18\x01 \x01(\tR\tplaintext\"=\n" + + "\x1bCryptoEncryptSecretResponse\x12\x1e\n" + + "\n" + + "ciphertext\x18\x01 \x01(\tR\n" + + "ciphertext\"<\n" + + "\x1aCryptoDecryptSecretRequest\x12\x1e\n" + + "\n" + + "ciphertext\x18\x01 \x01(\tR\n" + + "ciphertext\";\n" + + "\x1bCryptoDecryptSecretResponse\x12\x1c\n" + + "\tplaintext\x18\x01 \x01(\tR\tplaintext\"/\n" + + "\x19MenusGetMenuByNameRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\">\n" + + "\x1aMenusGetMenuByNameResponse\x12 \n" + + "\x04menu\x18\x01 \x01(\v2\f.abi.v1.MenuR\x04menu\"*\n" + + "\x04Menu\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"3\n" + + "\x18MenusGetMenuItemsRequest\x12\x17\n" + + "\amenu_id\x18\x01 \x01(\tR\x06menuId\"C\n" + + "\x19MenusGetMenuItemsResponse\x12&\n" + + "\x05items\x18\x01 \x03(\v2\x10.abi.v1.MenuItemR\x05items\"\xbc\x02\n" + + "\bMenuItem\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + + "\amenu_id\x18\x02 \x01(\tR\x06menuId\x12\x14\n" + + "\x05label\x18\x03 \x01(\tR\x05label\x12\x10\n" + + "\x03url\x18\x04 \x01(\tR\x03url\x12\x1b\n" + + "\tpage_slug\x18\x05 \x01(\tR\bpageSlug\x12 \n" + + "\tparent_id\x18\x06 \x01(\tH\x00R\bparentId\x88\x01\x01\x12\x1d\n" + + "\n" + + "sort_order\x18\a \x01(\x05R\tsortOrder\x12%\n" + + "\x0fopen_in_new_tab\x18\b \x01(\bR\fopenInNewTab\x12\x1b\n" + + "\tcss_class\x18\t \x01(\tR\bcssClass\x12\x1b\n" + + "\titem_type\x18\n" + + " \x01(\tR\bitemType\x12\x12\n" + + "\x04icon\x18\v \x01(\tR\x04iconB\f\n" + + "\n" + + "_parent_id\">\n" + + "\x1fDatasourcesResolveBucketRequest\x12\x1b\n" + + "\tbucket_id\x18\x01 \x01(\tR\bbucketId\"T\n" + + " DatasourcesResolveBucketResponse\x120\n" + + "\x06result\x18\x01 \x01(\v2\x18.abi.v1.DatasourceResultR\x06result\"E\n" + + "$DatasourcesResolveBucketByKeyRequest\x12\x1d\n" + + "\n" + + "bucket_key\x18\x01 \x01(\tR\tbucketKey\"Y\n" + + "%DatasourcesResolveBucketByKeyResponse\x120\n" + + "\x06result\x18\x01 \x01(\v2\x18.abi.v1.DatasourceResultR\x06result\"d\n" + + "\x10DatasourceResult\x12\x1d\n" + + "\n" + + "items_json\x18\x01 \x01(\fR\titemsJson\x12\x14\n" + + "\x05total\x18\x02 \x01(\x05R\x05total\x12\x1b\n" + + "\tmeta_json\x18\x03 \x01(\fR\bmetaJson\"7\n" + + "\x19UsersGetByUsernameRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\"K\n" + + "\x1aUsersGetByUsernameResponse\x12-\n" + + "\x04user\x18\x01 \x01(\v2\x19.abi.v1.PublicUserProfileR\x04user\"%\n" + + "\x13UsersGetByIdRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"E\n" + + "\x14UsersGetByIdResponse\x12-\n" + + "\x04user\x18\x01 \x01(\v2\x19.abi.v1.PublicUserProfileR\x04user\"\xe4\x01\n" + + "\x11PublicUserProfile\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\x12\x1a\n" + + "\busername\x18\x03 \x01(\tR\busername\x12!\n" + + "\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x1d\n" + + "\n" + + "avatar_url\x18\x05 \x01(\tR\tavatarUrl\x12\x10\n" + + "\x03bio\x18\x06 \x01(\tR\x03bio\x12%\n" + + "\x0eemail_verified\x18\a \x01(\bR\remailVerified\x12\x12\n" + + "\x04role\x18\b \x01(\tR\x04role\"?\n" + + "$SubscriptionsGetUserTierLevelRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\"Y\n" + + "%SubscriptionsGetUserTierLevelResponse\x120\n" + + "\n" + + "tier_level\x18\x01 \x01(\v2\x11.abi.v1.TierLevelR\ttierLevel\"=\n" + + "\tTierLevel\x12\x14\n" + + "\x05level\x18\x01 \x01(\x05R\x05level\x12\x1a\n" + + "\bfeatures\x18\x02 \x01(\fR\bfeatures\"7\n" + + "!SubscriptionsGetTierBySlugRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\"F\n" + + "\"SubscriptionsGetTierBySlugResponse\x12 \n" + + "\x04tier\x18\x01 \x01(\v2\f.abi.v1.TierR\x04tier\"\xcd\x01\n" + + "\x04Tier\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04slug\x18\x03 \x01(\tR\x04slug\x12\x14\n" + + "\x05level\x18\x04 \x01(\x05R\x05level\x12 \n" + + "\vdescription\x18\x05 \x01(\tR\vdescription\x12\x1a\n" + + "\bfeatures\x18\x06 \x01(\fR\bfeatures\x12\x1d\n" + + "\n" + + "is_default\x18\a \x01(\bR\tisDefault\x12\x1a\n" + + "\bposition\x18\b \x01(\x05R\bposition\"\x1f\n" + + "\x1dSubscriptionsListTiersRequest\"D\n" + + "\x1eSubscriptionsListTiersResponse\x12\"\n" + + "\x05tiers\x18\x01 \x03(\v2\f.abi.v1.TierR\x05tiers\">\n" + + "#SubscriptionsListActivePlansRequest\x12\x17\n" + + "\atier_id\x18\x01 \x01(\tR\x06tierId\"J\n" + + "$SubscriptionsListActivePlansResponse\x12\"\n" + + "\x05plans\x18\x01 \x03(\v2\f.abi.v1.PlanR\x05plans\"\xe6\x01\n" + + "\x04Plan\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + + "\atier_id\x18\x02 \x01(\tR\x06tierId\x12)\n" + + "\x10billing_interval\x18\x03 \x01(\tR\x0fbillingInterval\x12\x16\n" + + "\x06amount\x18\x04 \x01(\x05R\x06amount\x12\x1a\n" + + "\bcurrency\x18\x05 \x01(\tR\bcurrency\x12\x1b\n" + + "\tis_active\x18\x06 \x01(\bR\bisActive\x129\n" + + "\n" + + "created_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\xa0\x01\n" + + "\x13MediaDepositRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n" + + "\bfilename\x18\x02 \x01(\tR\bfilename\x12\x12\n" + + "\x04data\x18\x03 \x01(\fR\x04data\x12\x19\n" + + "\balt_text\x18\x04 \x01(\tR\aaltText\x12\x16\n" + + "\x06folder\x18\x05 \x01(\tR\x06folder\x12\x16\n" + + "\x06source\x18\x06 \x01(\tR\x06source\"R\n" + + "\x14MediaDepositResponse\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n" + + "\x03ref\x18\x02 \x01(\tR\x03ref\x12\x18\n" + + "\acreated\x18\x03 \x01(\bR\acreated\"P\n" + + "\x10EmailSendRequest\x12\x0e\n" + + "\x02to\x18\x01 \x01(\tR\x02to\x12\x18\n" + + "\asubject\x18\x02 \x01(\tR\asubject\x12\x12\n" + + "\x04body\x18\x03 \x01(\tR\x04body\"\x13\n" + + "\x11EmailSendResponse\"v\n" + + "\x11AiTextCallRequest\x12\x19\n" + + "\btask_key\x18\x01 \x01(\tR\ataskKey\x12#\n" + + "\rsystem_prompt\x18\x02 \x01(\tR\fsystemPrompt\x12!\n" + + "\fuser_message\x18\x03 \x01(\tR\vuserMessage\"(\n" + + "\x12AiTextCallResponse\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\"\x95\x01\n" + + "\x15AiToolRegisterRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x122\n" + + "\x15parameter_schema_json\x18\x04 \x01(\fR\x13parameterSchemaJson\"\x18\n" + + "\x16AiToolRegisterResponse\"b\n" + + "\x1cBridgeRegisterServiceRequest\x12\x1f\n" + + "\vplugin_name\x18\x01 \x01(\tR\n" + + "pluginName\x12!\n" + + "\fservice_name\x18\x02 \x01(\tR\vserviceName\"\x1f\n" + + "\x1dBridgeRegisterServiceResponse\"]\n" + + "\x17BridgeGetServiceRequest\x12\x1f\n" + + "\vplugin_name\x18\x01 \x01(\tR\n" + + "pluginName\x12!\n" + + "\fservice_name\x18\x02 \x01(\tR\vserviceName\"8\n" + + "\x18BridgeGetServiceResponse\x12\x1c\n" + + "\tavailable\x18\x01 \x01(\bR\tavailable\"O\n" + + "\x11JobsSubmitRequest\x12\x19\n" + + "\bjob_type\x18\x01 \x01(\tR\ajobType\x12\x1f\n" + + "\vconfig_json\x18\x02 \x01(\fR\n" + + "configJson\"\x14\n" + + "\x12JobsSubmitResponse\"8\n" + + "\"EmbeddingsGenerateEmbeddingRequest\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\"C\n" + + "#EmbeddingsGenerateEmbeddingResponse\x12\x1c\n" + + "\tembedding\x18\x01 \x03(\x02R\tembedding\"q\n" + + "\x1dEmbeddingsEmbedContentRequest\x12\x1f\n" + + "\vsource_type\x18\x01 \x01(\tR\n" + + "sourceType\x12\x1b\n" + + "\tsource_id\x18\x02 \x01(\tR\bsourceId\x12\x12\n" + + "\x04text\x18\x03 \x01(\tR\x04text\"<\n" + + "\x1eEmbeddingsEmbedContentResponse\x12\x1a\n" + + "\bembedded\x18\x01 \x01(\bR\bembedded\"\x1e\n" + + "\x1cEmbeddingsIsAvailableRequest\"=\n" + + "\x1dEmbeddingsIsAvailableResponse\x12\x1c\n" + + "\tavailable\x18\x01 \x01(\bR\tavailable\"=\n" + + "\x0fRagQueryRequest\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\"?\n" + + "\x10RagQueryResponse\x12+\n" + + "\aresults\x18\x01 \x03(\v2\x11.abi.v1.RagResultR\aresults\"\xb5\x01\n" + + "\tRagResult\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\x12\x14\n" + + "\x05score\x18\x02 \x01(\x01R\x05score\x12;\n" + + "\bmetadata\x18\x03 \x03(\v2\x1f.abi.v1.RagResult.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"^\n" + + "\x1aRagOnContentChangedRequest\x12!\n" + + "\fcontent_type\x18\x01 \x01(\tR\vcontentType\x12\x1d\n" + + "\n" + + "content_id\x18\x02 \x01(\tR\tcontentId\"\x1d\n" + + "\x1bRagOnContentChangedResponse\"\xd1\x01\n" + + "\x1aReviewsSubmitReviewRequest\x12\x19\n" + + "\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" + + "\x06row_id\x18\x02 \x01(\tR\x05rowId\x12%\n" + + "\x0eoverall_rating\x18\x03 \x01(\x05R\roverallRating\x12\x1f\n" + + "\vreview_text\x18\x04 \x01(\tR\n" + + "reviewText\x12!\n" + + "\fratings_json\x18\x05 \x01(\fR\vratingsJson\x12\x16\n" + + "\x06photos\x18\x06 \x03(\tR\x06photos\":\n" + + "\x1bReviewsSubmitReviewResponse\x12\x1b\n" + + "\treview_id\x18\x01 \x01(\tR\breviewId\"N\n" + + "\x1aBadgesRefreshBadgesRequest\x12\x19\n" + + "\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" + + "\x06row_id\x18\x02 \x01(\tR\x05rowId\"\x1d\n" + + "\x1bBadgesRefreshBadgesResponse\"\xb0\x01\n" + + "\x18ContentCreatePageRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" + + "\vparent_slug\x18\x02 \x01(\tR\n" + + "parentSlug\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\x12!\n" + + "\ftemplate_key\x18\x04 \x01(\tR\vtemplateKey\x12&\n" + + "\x0fmaster_page_key\x18\x05 \x01(\tR\rmasterPageKey\"N\n" + + "\x19ContentCreatePageResponse\x12\x17\n" + + "\apage_id\x18\x01 \x01(\tR\x06pageId\x12\x18\n" + + "\acreated\x18\x02 \x01(\bR\acreated\"\xcd\x01\n" + + "\tPageBlock\x12\x1b\n" + + "\tblock_key\x18\x01 \x01(\tR\bblockKey\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12!\n" + + "\fcontent_json\x18\x03 \x01(\fR\vcontentJson\x12&\n" + + "\fhtml_content\x18\x04 \x01(\tH\x00R\vhtmlContent\x88\x01\x01\x12\x12\n" + + "\x04slot\x18\x05 \x01(\tR\x04slot\x12\x1d\n" + + "\n" + + "sort_order\x18\x06 \x01(\x05R\tsortOrderB\x0f\n" + + "\r_html_content\"a\n" + + "\x1bContentSetPageBlocksRequest\x12\x17\n" + + "\apage_id\x18\x01 \x01(\tR\x06pageId\x12)\n" + + "\x06blocks\x18\x02 \x03(\v2\x11.abi.v1.PageBlockR\x06blocks\"\x1e\n" + + "\x1cContentSetPageBlocksResponse\"4\n" + + "\x19ContentPublishPageRequest\x12\x17\n" + + "\apage_id\x18\x01 \x01(\tR\x06pageId\"\x1c\n" + + "\x1aContentPublishPageResponse\"\xcd\x05\n" + + "\x18ContentSetPageSeoRequest\x12\x17\n" + + "\apage_id\x18\x01 \x01(\tR\x06pageId\x12\"\n" + + "\n" + + "meta_title\x18\x02 \x01(\tH\x00R\tmetaTitle\x88\x01\x01\x12.\n" + + "\x10meta_description\x18\x03 \x01(\tH\x01R\x0fmetaDescription\x88\x01\x01\x12\x1e\n" + + "\bog_title\x18\x04 \x01(\tH\x02R\aogTitle\x88\x01\x01\x12*\n" + + "\x0eog_description\x18\x05 \x01(\tH\x03R\rogDescription\x88\x01\x01\x12\x1e\n" + + "\bog_image\x18\x06 \x01(\tH\x04R\aogImage\x88\x01\x01\x12,\n" + + "\x0ffocus_keyphrase\x18\a \x01(\tH\x05R\x0efocusKeyphrase\x88\x01\x01\x12(\n" + + "\rcanonical_url\x18\b \x01(\tH\x06R\fcanonicalUrl\x88\x01\x01\x12.\n" + + "\x10robots_directive\x18\t \x01(\tH\aR\x0frobotsDirective\x88\x01\x01\x12(\n" + + "\rtwitter_title\x18\n" + + " \x01(\tH\bR\ftwitterTitle\x88\x01\x01\x124\n" + + "\x13twitter_description\x18\v \x01(\tH\tR\x12twitterDescription\x88\x01\x01\x12(\n" + + "\rtwitter_image\x18\f \x01(\tH\n" + + "R\ftwitterImage\x88\x01\x01B\r\n" + + "\v_meta_titleB\x13\n" + + "\x11_meta_descriptionB\v\n" + + "\t_og_titleB\x11\n" + + "\x0f_og_descriptionB\v\n" + + "\t_og_imageB\x12\n" + + "\x10_focus_keyphraseB\x10\n" + + "\x0e_canonical_urlB\x13\n" + + "\x11_robots_directiveB\x10\n" + + "\x0e_twitter_titleB\x16\n" + + "\x14_twitter_descriptionB\x10\n" + + "\x0e_twitter_image\"\x1b\n" + + "\x19ContentSetPageSeoResponse\"\xdd\x02\n" + + "\x18ContentUpsertPostRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12#\n" + + "\rdocument_json\x18\x03 \x01(\fR\fdocumentJson\x12\x1d\n" + + "\aexcerpt\x18\x04 \x01(\tH\x00R\aexcerpt\x88\x01\x01\x12/\n" + + "\x11author_profile_id\x18\x05 \x01(\tH\x01R\x0fauthorProfileId\x88\x01\x01\x12/\n" + + "\x11featured_image_id\x18\x06 \x01(\tH\x02R\x0ffeaturedImageId\x88\x01\x01\x12\x18\n" + + "\apublish\x18\a \x01(\bR\apublish\x12\x1f\n" + + "\vis_featured\x18\b \x01(\bR\n" + + "isFeaturedB\n" + + "\n" + + "\b_excerptB\x14\n" + + "\x12_author_profile_idB\x14\n" + + "\x12_featured_image_id\"N\n" + + "\x19ContentUpsertPostResponse\x12\x17\n" + + "\apost_id\x18\x01 \x01(\tR\x06postId\x12\x18\n" + + "\acreated\x18\x02 \x01(\bR\acreated\"\xad\x01\n" + + "!ProvisionerEnsureDataTableRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1f\n" + + "\vschema_json\x18\x04 \x01(\fR\n" + + "schemaJson\x12\x1f\n" + + "\vprimary_key\x18\x05 \x01(\tR\n" + + "primaryKey\"$\n" + + "\"ProvisionerEnsureDataTableResponse\"J\n" + + "#ProvisionerMergeSiteSettingsRequest\x12#\n" + + "\rdefaults_json\x18\x01 \x01(\fR\fdefaultsJson\"&\n" + + "$ProvisionerMergeSiteSettingsResponse\"a\n" + + "\x1fProvisionerEnsureSettingRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12,\n" + + "\x12default_value_json\x18\x02 \x01(\fR\x10defaultValueJson\"\"\n" + + " ProvisionerEnsureSettingResponse\"\x83\x03\n" + + "\bPageSeed\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" + + "\vparent_slug\x18\x02 \x01(\tR\n" + + "parentSlug\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\x12!\n" + + "\ftemplate_key\x18\x04 \x01(\tR\vtemplateKey\x12)\n" + + "\x06blocks\x18\x05 \x03(\v2\x11.abi.v1.PageBlockR\x06blocks\x12,\n" + + "\x12detail_source_type\x18\x06 \x01(\tR\x10detailSourceType\x12*\n" + + "\x11detail_source_key\x18\a \x01(\tR\x0fdetailSourceKey\x12*\n" + + "\x11detail_slug_field\x18\b \x01(\tR\x0fdetailSlugField\x12)\n" + + "\x10reconcile_blocks\x18\t \x01(\bR\x0freconcileBlocks\x12-\n" + + "\x12reconcile_template\x18\n" + + " \x01(\bR\x11reconcileTemplate\"D\n" + + "\x1cProvisionerEnsurePageRequest\x12$\n" + + "\x04page\x18\x01 \x01(\v2\x10.abi.v1.PageSeedR\x04page\"\x1f\n" + + "\x1dProvisionerEnsurePageResponse\"O\n" + + "&ProvisionerOverrideSiteSettingsRequest\x12%\n" + + "\x0eoverrides_json\x18\x01 \x01(\fR\roverridesJson\")\n" + + "'ProvisionerOverrideSiteSettingsResponse\"\xa3\x01\n" + + " ProvisionerEnsureMenuItemRequest\x12\x1b\n" + + "\tmenu_name\x18\x01 \x01(\tR\bmenuName\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\x12\x10\n" + + "\x03url\x18\x03 \x01(\tR\x03url\x12\x1b\n" + + "\tpage_slug\x18\x04 \x01(\tR\bpageSlug\x12\x1d\n" + + "\n" + + "sort_order\x18\x05 \x01(\x05R\tsortOrder\"#\n" + + "!ProvisionerEnsureMenuItemResponse\"\x87\x01\n" + + ")ProvisionerRegisterEmbeddingConfigRequest\x12\x1b\n" + + "\ttable_key\x18\x01 \x01(\tR\btableKey\x12#\n" + + "\rtext_template\x18\x02 \x01(\tR\ftextTemplate\x12\x18\n" + + "\aenabled\x18\x03 \x01(\bR\aenabled\",\n" + + "*ProvisionerRegisterEmbeddingConfigResponse\"\x97\x02\n" + + "\x1dProvisionerEnsureEmbedRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x12\n" + + "\x04icon\x18\x04 \x01(\tR\x04icon\x12\x1f\n" + + "\vlabel_field\x18\x05 \x01(\tR\n" + + "labelField\x12\x1a\n" + + "\btemplate\x18\x06 \x01(\tR\btemplate\x12(\n" + + "\x10data_source_type\x18\a \x01(\tR\x0edataSourceType\x121\n" + + "\x15data_source_table_key\x18\b \x01(\tR\x12dataSourceTableKey\" \n" + + "\x1eProvisionerEnsureEmbedResponse\"\x8a\x01\n" + + "#ProvisionerEnsureJobScheduleRequest\x12\x19\n" + + "\bjob_type\x18\x01 \x01(\tR\ajobType\x12'\n" + + "\x0fcron_expression\x18\x02 \x01(\tR\x0ecronExpression\x12\x1f\n" + + "\vconfig_json\x18\x03 \x01(\fR\n" + + "configJson\"&\n" + + "$ProvisionerEnsureJobScheduleResponse\"~\n" + + ")ProvisionerUpdateDataTableRowFieldRequest\x12\x15\n" + + "\x06row_id\x18\x01 \x01(\tR\x05rowId\x12\x1b\n" + + "\tfield_key\x18\x02 \x01(\tR\bfieldKey\x12\x1d\n" + + "\n" + + "value_json\x18\x03 \x01(\fR\tvalueJson\",\n" + + "*ProvisionerUpdateDataTableRowFieldResponse\"Z\n" + + "-ProvisionerDisableOrphanedJobSchedulesRequest\x12)\n" + + "\x10registered_types\x18\x01 \x03(\tR\x0fregisteredTypes\"0\n" + + ".ProvisionerDisableOrphanedJobSchedulesResponse\"4\n" + + "\x1eProvisionerEnsurePluginRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"!\n" + + "\x1fProvisionerEnsurePluginResponse\"\x91\x01\n" + + "#ProvisionerEnsureCustomColorRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1f\n" + + "\vlight_value\x18\x02 \x01(\tR\n" + + "lightValue\x12\x1d\n" + + "\n" + + "dark_value\x18\x03 \x01(\tR\tdarkValue\x12\x16\n" + + "\x06source\x18\x04 \x01(\tR\x06source\"&\n" + + "$ProvisionerEnsureCustomColorResponse\"\xaa\x01\n" + + "\x1dProvisionerEnsureMediaRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n" + + "\bfilename\x18\x02 \x01(\tR\bfilename\x12\x12\n" + + "\x04data\x18\x03 \x01(\fR\x04data\x12\x19\n" + + "\balt_text\x18\x04 \x01(\tR\aaltText\x12\x16\n" + + "\x06folder\x18\x05 \x01(\tR\x06folder\x12\x16\n" + + "\x06source\x18\x06 \x01(\tR\x06source\" \n" + + "\x1eProvisionerEnsureMediaResponse\"k\n" + + "#SettingsUpdatePluginSettingsRequest\x12\x1f\n" + + "\vplugin_name\x18\x01 \x01(\tR\n" + + "pluginName\x12#\n" + + "\rsettings_json\x18\x02 \x01(\fR\fsettingsJson\"&\n" + + "$SettingsUpdatePluginSettingsResponse\"_\n" + + "\x13JobsProgressRequest\x12\x18\n" + + "\acurrent\x18\x01 \x01(\x05R\acurrent\x12\x14\n" + + "\x05total\x18\x02 \x01(\x05R\x05total\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\"\x16\n" + + "\x14JobsProgressResponse\"\x8b\x01\n" + + "\x13BridgeInvokeRequest\x12\x1f\n" + + "\vplugin_name\x18\x01 \x01(\tR\n" + + "pluginName\x12!\n" + + "\fservice_name\x18\x02 \x01(\tR\vserviceName\x12\x16\n" + + "\x06method\x18\x03 \x01(\tR\x06method\x12\x18\n" + + "\apayload\x18\x04 \x01(\fR\apayload\"0\n" + + "\x14BridgeInvokeResponse\x12\x18\n" + + "\apayload\x18\x01 \x01(\fR\apayloadB5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3" + +var ( + file_v1_capability_proto_rawDescOnce sync.Once + file_v1_capability_proto_rawDescData []byte +) + +func file_v1_capability_proto_rawDescGZIP() []byte { + file_v1_capability_proto_rawDescOnce.Do(func() { + file_v1_capability_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_capability_proto_rawDesc), len(file_v1_capability_proto_rawDesc))) + }) + return file_v1_capability_proto_rawDescData +} + +var file_v1_capability_proto_msgTypes = make([]protoimpl.MessageInfo, 141) +var file_v1_capability_proto_goTypes = []any{ + (*HostCallRequest)(nil), // 0: abi.v1.HostCallRequest + (*HostCallResponse)(nil), // 1: abi.v1.HostCallResponse + (*ContentGetAuthorProfileRequest)(nil), // 2: abi.v1.ContentGetAuthorProfileRequest + (*ContentGetAuthorProfileResponse)(nil), // 3: abi.v1.ContentGetAuthorProfileResponse + (*AuthorProfile)(nil), // 4: abi.v1.AuthorProfile + (*ContentGetPageRequest)(nil), // 5: abi.v1.ContentGetPageRequest + (*ContentGetPageResponse)(nil), // 6: abi.v1.ContentGetPageResponse + (*PageInfo)(nil), // 7: abi.v1.PageInfo + (*ContentGetPostRequest)(nil), // 8: abi.v1.ContentGetPostRequest + (*ContentGetPostResponse)(nil), // 9: abi.v1.ContentGetPostResponse + (*PostInfo)(nil), // 10: abi.v1.PostInfo + (*ContentListPostsRequest)(nil), // 11: abi.v1.ContentListPostsRequest + (*ContentListPostsResponse)(nil), // 12: abi.v1.ContentListPostsResponse + (*ContentSlugifyRequest)(nil), // 13: abi.v1.ContentSlugifyRequest + (*ContentSlugifyResponse)(nil), // 14: abi.v1.ContentSlugifyResponse + (*ContentBlockNoteToHtmlRequest)(nil), // 15: abi.v1.ContentBlockNoteToHtmlRequest + (*ContentBlockNoteToHtmlResponse)(nil), // 16: abi.v1.ContentBlockNoteToHtmlResponse + (*ContentGenerateExcerptRequest)(nil), // 17: abi.v1.ContentGenerateExcerptRequest + (*ContentGenerateExcerptResponse)(nil), // 18: abi.v1.ContentGenerateExcerptResponse + (*ContentStripHtmlRequest)(nil), // 19: abi.v1.ContentStripHtmlRequest + (*ContentStripHtmlResponse)(nil), // 20: abi.v1.ContentStripHtmlResponse + (*SettingsGetSiteSettingsRequest)(nil), // 21: abi.v1.SettingsGetSiteSettingsRequest + (*SettingsGetSiteSettingsResponse)(nil), // 22: abi.v1.SettingsGetSiteSettingsResponse + (*SettingsGetPluginSettingsRequest)(nil), // 23: abi.v1.SettingsGetPluginSettingsRequest + (*SettingsGetPluginSettingsResponse)(nil), // 24: abi.v1.SettingsGetPluginSettingsResponse + (*SettingsUpdateSiteSettingRequest)(nil), // 25: abi.v1.SettingsUpdateSiteSettingRequest + (*SettingsUpdateSiteSettingResponse)(nil), // 26: abi.v1.SettingsUpdateSiteSettingResponse + (*GatingGetSubscriberTierLevelRequest)(nil), // 27: abi.v1.GatingGetSubscriberTierLevelRequest + (*GatingGetSubscriberTierLevelResponse)(nil), // 28: abi.v1.GatingGetSubscriberTierLevelResponse + (*GatingEvaluateAccessRequest)(nil), // 29: abi.v1.GatingEvaluateAccessRequest + (*GatingEvaluateAccessResponse)(nil), // 30: abi.v1.GatingEvaluateAccessResponse + (*AccessRule)(nil), // 31: abi.v1.AccessRule + (*AccessResult)(nil), // 32: abi.v1.AccessResult + (*CryptoEncryptSecretRequest)(nil), // 33: abi.v1.CryptoEncryptSecretRequest + (*CryptoEncryptSecretResponse)(nil), // 34: abi.v1.CryptoEncryptSecretResponse + (*CryptoDecryptSecretRequest)(nil), // 35: abi.v1.CryptoDecryptSecretRequest + (*CryptoDecryptSecretResponse)(nil), // 36: abi.v1.CryptoDecryptSecretResponse + (*MenusGetMenuByNameRequest)(nil), // 37: abi.v1.MenusGetMenuByNameRequest + (*MenusGetMenuByNameResponse)(nil), // 38: abi.v1.MenusGetMenuByNameResponse + (*Menu)(nil), // 39: abi.v1.Menu + (*MenusGetMenuItemsRequest)(nil), // 40: abi.v1.MenusGetMenuItemsRequest + (*MenusGetMenuItemsResponse)(nil), // 41: abi.v1.MenusGetMenuItemsResponse + (*MenuItem)(nil), // 42: abi.v1.MenuItem + (*DatasourcesResolveBucketRequest)(nil), // 43: abi.v1.DatasourcesResolveBucketRequest + (*DatasourcesResolveBucketResponse)(nil), // 44: abi.v1.DatasourcesResolveBucketResponse + (*DatasourcesResolveBucketByKeyRequest)(nil), // 45: abi.v1.DatasourcesResolveBucketByKeyRequest + (*DatasourcesResolveBucketByKeyResponse)(nil), // 46: abi.v1.DatasourcesResolveBucketByKeyResponse + (*DatasourceResult)(nil), // 47: abi.v1.DatasourceResult + (*UsersGetByUsernameRequest)(nil), // 48: abi.v1.UsersGetByUsernameRequest + (*UsersGetByUsernameResponse)(nil), // 49: abi.v1.UsersGetByUsernameResponse + (*UsersGetByIdRequest)(nil), // 50: abi.v1.UsersGetByIdRequest + (*UsersGetByIdResponse)(nil), // 51: abi.v1.UsersGetByIdResponse + (*PublicUserProfile)(nil), // 52: abi.v1.PublicUserProfile + (*SubscriptionsGetUserTierLevelRequest)(nil), // 53: abi.v1.SubscriptionsGetUserTierLevelRequest + (*SubscriptionsGetUserTierLevelResponse)(nil), // 54: abi.v1.SubscriptionsGetUserTierLevelResponse + (*TierLevel)(nil), // 55: abi.v1.TierLevel + (*SubscriptionsGetTierBySlugRequest)(nil), // 56: abi.v1.SubscriptionsGetTierBySlugRequest + (*SubscriptionsGetTierBySlugResponse)(nil), // 57: abi.v1.SubscriptionsGetTierBySlugResponse + (*Tier)(nil), // 58: abi.v1.Tier + (*SubscriptionsListTiersRequest)(nil), // 59: abi.v1.SubscriptionsListTiersRequest + (*SubscriptionsListTiersResponse)(nil), // 60: abi.v1.SubscriptionsListTiersResponse + (*SubscriptionsListActivePlansRequest)(nil), // 61: abi.v1.SubscriptionsListActivePlansRequest + (*SubscriptionsListActivePlansResponse)(nil), // 62: abi.v1.SubscriptionsListActivePlansResponse + (*Plan)(nil), // 63: abi.v1.Plan + (*MediaDepositRequest)(nil), // 64: abi.v1.MediaDepositRequest + (*MediaDepositResponse)(nil), // 65: abi.v1.MediaDepositResponse + (*EmailSendRequest)(nil), // 66: abi.v1.EmailSendRequest + (*EmailSendResponse)(nil), // 67: abi.v1.EmailSendResponse + (*AiTextCallRequest)(nil), // 68: abi.v1.AiTextCallRequest + (*AiTextCallResponse)(nil), // 69: abi.v1.AiTextCallResponse + (*AiToolRegisterRequest)(nil), // 70: abi.v1.AiToolRegisterRequest + (*AiToolRegisterResponse)(nil), // 71: abi.v1.AiToolRegisterResponse + (*BridgeRegisterServiceRequest)(nil), // 72: abi.v1.BridgeRegisterServiceRequest + (*BridgeRegisterServiceResponse)(nil), // 73: abi.v1.BridgeRegisterServiceResponse + (*BridgeGetServiceRequest)(nil), // 74: abi.v1.BridgeGetServiceRequest + (*BridgeGetServiceResponse)(nil), // 75: abi.v1.BridgeGetServiceResponse + (*JobsSubmitRequest)(nil), // 76: abi.v1.JobsSubmitRequest + (*JobsSubmitResponse)(nil), // 77: abi.v1.JobsSubmitResponse + (*EmbeddingsGenerateEmbeddingRequest)(nil), // 78: abi.v1.EmbeddingsGenerateEmbeddingRequest + (*EmbeddingsGenerateEmbeddingResponse)(nil), // 79: abi.v1.EmbeddingsGenerateEmbeddingResponse + (*EmbeddingsEmbedContentRequest)(nil), // 80: abi.v1.EmbeddingsEmbedContentRequest + (*EmbeddingsEmbedContentResponse)(nil), // 81: abi.v1.EmbeddingsEmbedContentResponse + (*EmbeddingsIsAvailableRequest)(nil), // 82: abi.v1.EmbeddingsIsAvailableRequest + (*EmbeddingsIsAvailableResponse)(nil), // 83: abi.v1.EmbeddingsIsAvailableResponse + (*RagQueryRequest)(nil), // 84: abi.v1.RagQueryRequest + (*RagQueryResponse)(nil), // 85: abi.v1.RagQueryResponse + (*RagResult)(nil), // 86: abi.v1.RagResult + (*RagOnContentChangedRequest)(nil), // 87: abi.v1.RagOnContentChangedRequest + (*RagOnContentChangedResponse)(nil), // 88: abi.v1.RagOnContentChangedResponse + (*ReviewsSubmitReviewRequest)(nil), // 89: abi.v1.ReviewsSubmitReviewRequest + (*ReviewsSubmitReviewResponse)(nil), // 90: abi.v1.ReviewsSubmitReviewResponse + (*BadgesRefreshBadgesRequest)(nil), // 91: abi.v1.BadgesRefreshBadgesRequest + (*BadgesRefreshBadgesResponse)(nil), // 92: abi.v1.BadgesRefreshBadgesResponse + (*ContentCreatePageRequest)(nil), // 93: abi.v1.ContentCreatePageRequest + (*ContentCreatePageResponse)(nil), // 94: abi.v1.ContentCreatePageResponse + (*PageBlock)(nil), // 95: abi.v1.PageBlock + (*ContentSetPageBlocksRequest)(nil), // 96: abi.v1.ContentSetPageBlocksRequest + (*ContentSetPageBlocksResponse)(nil), // 97: abi.v1.ContentSetPageBlocksResponse + (*ContentPublishPageRequest)(nil), // 98: abi.v1.ContentPublishPageRequest + (*ContentPublishPageResponse)(nil), // 99: abi.v1.ContentPublishPageResponse + (*ContentSetPageSeoRequest)(nil), // 100: abi.v1.ContentSetPageSeoRequest + (*ContentSetPageSeoResponse)(nil), // 101: abi.v1.ContentSetPageSeoResponse + (*ContentUpsertPostRequest)(nil), // 102: abi.v1.ContentUpsertPostRequest + (*ContentUpsertPostResponse)(nil), // 103: abi.v1.ContentUpsertPostResponse + (*ProvisionerEnsureDataTableRequest)(nil), // 104: abi.v1.ProvisionerEnsureDataTableRequest + (*ProvisionerEnsureDataTableResponse)(nil), // 105: abi.v1.ProvisionerEnsureDataTableResponse + (*ProvisionerMergeSiteSettingsRequest)(nil), // 106: abi.v1.ProvisionerMergeSiteSettingsRequest + (*ProvisionerMergeSiteSettingsResponse)(nil), // 107: abi.v1.ProvisionerMergeSiteSettingsResponse + (*ProvisionerEnsureSettingRequest)(nil), // 108: abi.v1.ProvisionerEnsureSettingRequest + (*ProvisionerEnsureSettingResponse)(nil), // 109: abi.v1.ProvisionerEnsureSettingResponse + (*PageSeed)(nil), // 110: abi.v1.PageSeed + (*ProvisionerEnsurePageRequest)(nil), // 111: abi.v1.ProvisionerEnsurePageRequest + (*ProvisionerEnsurePageResponse)(nil), // 112: abi.v1.ProvisionerEnsurePageResponse + (*ProvisionerOverrideSiteSettingsRequest)(nil), // 113: abi.v1.ProvisionerOverrideSiteSettingsRequest + (*ProvisionerOverrideSiteSettingsResponse)(nil), // 114: abi.v1.ProvisionerOverrideSiteSettingsResponse + (*ProvisionerEnsureMenuItemRequest)(nil), // 115: abi.v1.ProvisionerEnsureMenuItemRequest + (*ProvisionerEnsureMenuItemResponse)(nil), // 116: abi.v1.ProvisionerEnsureMenuItemResponse + (*ProvisionerRegisterEmbeddingConfigRequest)(nil), // 117: abi.v1.ProvisionerRegisterEmbeddingConfigRequest + (*ProvisionerRegisterEmbeddingConfigResponse)(nil), // 118: abi.v1.ProvisionerRegisterEmbeddingConfigResponse + (*ProvisionerEnsureEmbedRequest)(nil), // 119: abi.v1.ProvisionerEnsureEmbedRequest + (*ProvisionerEnsureEmbedResponse)(nil), // 120: abi.v1.ProvisionerEnsureEmbedResponse + (*ProvisionerEnsureJobScheduleRequest)(nil), // 121: abi.v1.ProvisionerEnsureJobScheduleRequest + (*ProvisionerEnsureJobScheduleResponse)(nil), // 122: abi.v1.ProvisionerEnsureJobScheduleResponse + (*ProvisionerUpdateDataTableRowFieldRequest)(nil), // 123: abi.v1.ProvisionerUpdateDataTableRowFieldRequest + (*ProvisionerUpdateDataTableRowFieldResponse)(nil), // 124: abi.v1.ProvisionerUpdateDataTableRowFieldResponse + (*ProvisionerDisableOrphanedJobSchedulesRequest)(nil), // 125: abi.v1.ProvisionerDisableOrphanedJobSchedulesRequest + (*ProvisionerDisableOrphanedJobSchedulesResponse)(nil), // 126: abi.v1.ProvisionerDisableOrphanedJobSchedulesResponse + (*ProvisionerEnsurePluginRequest)(nil), // 127: abi.v1.ProvisionerEnsurePluginRequest + (*ProvisionerEnsurePluginResponse)(nil), // 128: abi.v1.ProvisionerEnsurePluginResponse + (*ProvisionerEnsureCustomColorRequest)(nil), // 129: abi.v1.ProvisionerEnsureCustomColorRequest + (*ProvisionerEnsureCustomColorResponse)(nil), // 130: abi.v1.ProvisionerEnsureCustomColorResponse + (*ProvisionerEnsureMediaRequest)(nil), // 131: abi.v1.ProvisionerEnsureMediaRequest + (*ProvisionerEnsureMediaResponse)(nil), // 132: abi.v1.ProvisionerEnsureMediaResponse + (*SettingsUpdatePluginSettingsRequest)(nil), // 133: abi.v1.SettingsUpdatePluginSettingsRequest + (*SettingsUpdatePluginSettingsResponse)(nil), // 134: abi.v1.SettingsUpdatePluginSettingsResponse + (*JobsProgressRequest)(nil), // 135: abi.v1.JobsProgressRequest + (*JobsProgressResponse)(nil), // 136: abi.v1.JobsProgressResponse + (*BridgeInvokeRequest)(nil), // 137: abi.v1.BridgeInvokeRequest + (*BridgeInvokeResponse)(nil), // 138: abi.v1.BridgeInvokeResponse + nil, // 139: abi.v1.AuthorProfile.SocialLinksEntry + nil, // 140: abi.v1.RagResult.MetadataEntry + (*AbiError)(nil), // 141: abi.v1.AbiError + (*timestamppb.Timestamp)(nil), // 142: google.protobuf.Timestamp +} +var file_v1_capability_proto_depIdxs = []int32{ + 141, // 0: abi.v1.HostCallResponse.error:type_name -> abi.v1.AbiError + 4, // 1: abi.v1.ContentGetAuthorProfileResponse.author:type_name -> abi.v1.AuthorProfile + 139, // 2: abi.v1.AuthorProfile.social_links:type_name -> abi.v1.AuthorProfile.SocialLinksEntry + 7, // 3: abi.v1.ContentGetPageResponse.page:type_name -> abi.v1.PageInfo + 10, // 4: abi.v1.ContentGetPostResponse.post:type_name -> abi.v1.PostInfo + 142, // 5: abi.v1.PostInfo.published_at:type_name -> google.protobuf.Timestamp + 10, // 6: abi.v1.ContentListPostsResponse.posts:type_name -> abi.v1.PostInfo + 31, // 7: abi.v1.GatingEvaluateAccessRequest.rule:type_name -> abi.v1.AccessRule + 32, // 8: abi.v1.GatingEvaluateAccessResponse.result:type_name -> abi.v1.AccessResult + 39, // 9: abi.v1.MenusGetMenuByNameResponse.menu:type_name -> abi.v1.Menu + 42, // 10: abi.v1.MenusGetMenuItemsResponse.items:type_name -> abi.v1.MenuItem + 47, // 11: abi.v1.DatasourcesResolveBucketResponse.result:type_name -> abi.v1.DatasourceResult + 47, // 12: abi.v1.DatasourcesResolveBucketByKeyResponse.result:type_name -> abi.v1.DatasourceResult + 52, // 13: abi.v1.UsersGetByUsernameResponse.user:type_name -> abi.v1.PublicUserProfile + 52, // 14: abi.v1.UsersGetByIdResponse.user:type_name -> abi.v1.PublicUserProfile + 55, // 15: abi.v1.SubscriptionsGetUserTierLevelResponse.tier_level:type_name -> abi.v1.TierLevel + 58, // 16: abi.v1.SubscriptionsGetTierBySlugResponse.tier:type_name -> abi.v1.Tier + 58, // 17: abi.v1.SubscriptionsListTiersResponse.tiers:type_name -> abi.v1.Tier + 63, // 18: abi.v1.SubscriptionsListActivePlansResponse.plans:type_name -> abi.v1.Plan + 142, // 19: abi.v1.Plan.created_at:type_name -> google.protobuf.Timestamp + 86, // 20: abi.v1.RagQueryResponse.results:type_name -> abi.v1.RagResult + 140, // 21: abi.v1.RagResult.metadata:type_name -> abi.v1.RagResult.MetadataEntry + 95, // 22: abi.v1.ContentSetPageBlocksRequest.blocks:type_name -> abi.v1.PageBlock + 95, // 23: abi.v1.PageSeed.blocks:type_name -> abi.v1.PageBlock + 110, // 24: abi.v1.ProvisionerEnsurePageRequest.page:type_name -> abi.v1.PageSeed + 25, // [25:25] is the sub-list for method output_type + 25, // [25:25] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name +} + +func init() { file_v1_capability_proto_init() } +func file_v1_capability_proto_init() { + if File_v1_capability_proto != nil { + return + } + file_v1_invoke_proto_init() + file_v1_capability_proto_msgTypes[42].OneofWrappers = []any{} + file_v1_capability_proto_msgTypes[95].OneofWrappers = []any{} + file_v1_capability_proto_msgTypes[100].OneofWrappers = []any{} + file_v1_capability_proto_msgTypes[102].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_capability_proto_rawDesc), len(file_v1_capability_proto_rawDesc)), + NumEnums: 0, + NumMessages: 141, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_capability_proto_goTypes, + DependencyIndexes: file_v1_capability_proto_depIdxs, + MessageInfos: file_v1_capability_proto_msgTypes, + }.Build() + File_v1_capability_proto = out.File + file_v1_capability_proto_goTypes = nil + file_v1_capability_proto_depIdxs = nil +} diff --git a/abi/v1/db.pb.go b/abi/v1/db.pb.go new file mode 100644 index 0000000..7091837 --- /dev/null +++ b/abi/v1/db.pb.go @@ -0,0 +1,1128 @@ +// db.proto — DB driver messages (WO-WZ-001). +// +// The guest SDK ships a database/sql driver that marshals query text + args +// out through these messages and rows back, so sqlc-generated plugin code +// works unchanged. The host executes on a connection under the per-plugin +// Postgres role. Transactions map to a host-side handle held per guest call +// chain, with a hard deadline so a guest can never pin a connection. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: v1/db.proto + +package abiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// DbValue is one pgx-mappable parameter or column value. +type DbValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Kind: + // + // *DbValue_Null + // *DbValue_BoolValue + // *DbValue_Int64Value + // *DbValue_Float64Value + // *DbValue_StringValue + // *DbValue_BytesValue + // *DbValue_TimestampValue + // *DbValue_UuidValue + // *DbValue_JsonbValue + // *DbValue_NumericValue + // *DbValue_TextArrayValue + // *DbValue_UuidArrayValue + Kind isDbValue_Kind `protobuf_oneof:"kind"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbValue) Reset() { + *x = DbValue{} + mi := &file_v1_db_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbValue) ProtoMessage() {} + +func (x *DbValue) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbValue.ProtoReflect.Descriptor instead. +func (*DbValue) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{0} +} + +func (x *DbValue) GetKind() isDbValue_Kind { + if x != nil { + return x.Kind + } + return nil +} + +func (x *DbValue) GetNull() bool { + if x != nil { + if x, ok := x.Kind.(*DbValue_Null); ok { + return x.Null + } + } + return false +} + +func (x *DbValue) GetBoolValue() bool { + if x != nil { + if x, ok := x.Kind.(*DbValue_BoolValue); ok { + return x.BoolValue + } + } + return false +} + +func (x *DbValue) GetInt64Value() int64 { + if x != nil { + if x, ok := x.Kind.(*DbValue_Int64Value); ok { + return x.Int64Value + } + } + return 0 +} + +func (x *DbValue) GetFloat64Value() float64 { + if x != nil { + if x, ok := x.Kind.(*DbValue_Float64Value); ok { + return x.Float64Value + } + } + return 0 +} + +func (x *DbValue) GetStringValue() string { + if x != nil { + if x, ok := x.Kind.(*DbValue_StringValue); ok { + return x.StringValue + } + } + return "" +} + +func (x *DbValue) GetBytesValue() []byte { + if x != nil { + if x, ok := x.Kind.(*DbValue_BytesValue); ok { + return x.BytesValue + } + } + return nil +} + +func (x *DbValue) GetTimestampValue() *timestamppb.Timestamp { + if x != nil { + if x, ok := x.Kind.(*DbValue_TimestampValue); ok { + return x.TimestampValue + } + } + return nil +} + +func (x *DbValue) GetUuidValue() string { + if x != nil { + if x, ok := x.Kind.(*DbValue_UuidValue); ok { + return x.UuidValue + } + } + return "" +} + +func (x *DbValue) GetJsonbValue() []byte { + if x != nil { + if x, ok := x.Kind.(*DbValue_JsonbValue); ok { + return x.JsonbValue + } + } + return nil +} + +func (x *DbValue) GetNumericValue() string { + if x != nil { + if x, ok := x.Kind.(*DbValue_NumericValue); ok { + return x.NumericValue + } + } + return "" +} + +func (x *DbValue) GetTextArrayValue() *TextArray { + if x != nil { + if x, ok := x.Kind.(*DbValue_TextArrayValue); ok { + return x.TextArrayValue + } + } + return nil +} + +func (x *DbValue) GetUuidArrayValue() *UuidArray { + if x != nil { + if x, ok := x.Kind.(*DbValue_UuidArrayValue); ok { + return x.UuidArrayValue + } + } + return nil +} + +type isDbValue_Kind interface { + isDbValue_Kind() +} + +type DbValue_Null struct { + // SQL NULL (the bool carries no information; true by convention). + Null bool `protobuf:"varint,1,opt,name=null,proto3,oneof"` +} + +type DbValue_BoolValue struct { + BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` +} + +type DbValue_Int64Value struct { + Int64Value int64 `protobuf:"varint,3,opt,name=int64_value,json=int64Value,proto3,oneof"` +} + +type DbValue_Float64Value struct { + Float64Value float64 `protobuf:"fixed64,4,opt,name=float64_value,json=float64Value,proto3,oneof"` +} + +type DbValue_StringValue struct { + StringValue string `protobuf:"bytes,5,opt,name=string_value,json=stringValue,proto3,oneof"` +} + +type DbValue_BytesValue struct { + BytesValue []byte `protobuf:"bytes,6,opt,name=bytes_value,json=bytesValue,proto3,oneof"` +} + +type DbValue_TimestampValue struct { + TimestampValue *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=timestamp_value,json=timestampValue,proto3,oneof"` +} + +type DbValue_UuidValue struct { + // UUID in canonical string form. + UuidValue string `protobuf:"bytes,8,opt,name=uuid_value,json=uuidValue,proto3,oneof"` +} + +type DbValue_JsonbValue struct { + // JSON/JSONB payload bytes. + JsonbValue []byte `protobuf:"bytes,9,opt,name=jsonb_value,json=jsonbValue,proto3,oneof"` +} + +type DbValue_NumericValue struct { + // NUMERIC in decimal string form (lossless). + NumericValue string `protobuf:"bytes,10,opt,name=numeric_value,json=numericValue,proto3,oneof"` +} + +type DbValue_TextArrayValue struct { + // text[] array. + TextArrayValue *TextArray `protobuf:"bytes,11,opt,name=text_array_value,json=textArrayValue,proto3,oneof"` +} + +type DbValue_UuidArrayValue struct { + // uuid[] array (each element canonical string form). Distinct from + // text_array so the host binds a native uuid[] parameter (letting a query + // keep `ANY($1::uuid[])` with no text[]-cast workaround) and scans a uuid[] + // column straight into []uuid.UUID. + UuidArrayValue *UuidArray `protobuf:"bytes,12,opt,name=uuid_array_value,json=uuidArrayValue,proto3,oneof"` +} + +func (*DbValue_Null) isDbValue_Kind() {} + +func (*DbValue_BoolValue) isDbValue_Kind() {} + +func (*DbValue_Int64Value) isDbValue_Kind() {} + +func (*DbValue_Float64Value) isDbValue_Kind() {} + +func (*DbValue_StringValue) isDbValue_Kind() {} + +func (*DbValue_BytesValue) isDbValue_Kind() {} + +func (*DbValue_TimestampValue) isDbValue_Kind() {} + +func (*DbValue_UuidValue) isDbValue_Kind() {} + +func (*DbValue_JsonbValue) isDbValue_Kind() {} + +func (*DbValue_NumericValue) isDbValue_Kind() {} + +func (*DbValue_TextArrayValue) isDbValue_Kind() {} + +func (*DbValue_UuidArrayValue) isDbValue_Kind() {} + +// TextArray is a Postgres text[] value. +type TextArray struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TextArray) Reset() { + *x = TextArray{} + mi := &file_v1_db_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TextArray) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TextArray) ProtoMessage() {} + +func (x *TextArray) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TextArray.ProtoReflect.Descriptor instead. +func (*TextArray) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{1} +} + +func (x *TextArray) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +// UuidArray is a Postgres uuid[] value; each element is a canonical UUID +// string (matching DbValue.uuid_value's single-UUID encoding). +type UuidArray struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UuidArray) Reset() { + *x = UuidArray{} + mi := &file_v1_db_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UuidArray) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UuidArray) ProtoMessage() {} + +func (x *UuidArray) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UuidArray.ProtoReflect.Descriptor instead. +func (*UuidArray) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{2} +} + +func (x *UuidArray) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +// DbRow is one result row; values align with DbRowsResponse.columns. +type DbRow struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values []*DbValue `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbRow) Reset() { + *x = DbRow{} + mi := &file_v1_db_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbRow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbRow) ProtoMessage() {} + +func (x *DbRow) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbRow.ProtoReflect.Descriptor instead. +func (*DbRow) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{3} +} + +func (x *DbRow) GetValues() []*DbValue { + if x != nil { + return x.Values + } + return nil +} + +// DbError carries a database failure back to the guest driver. +type DbError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Postgres SQLSTATE when available (e.g. "23505"); empty otherwise. + Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbError) Reset() { + *x = DbError{} + mi := &file_v1_db_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbError) ProtoMessage() {} + +func (x *DbError) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbError.ProtoReflect.Descriptor instead. +func (*DbError) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{4} +} + +func (x *DbError) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *DbError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// DbQueryRequest executes a rows-returning statement. +type DbQueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sql string `protobuf:"bytes,1,opt,name=sql,proto3" json:"sql,omitempty"` + Args []*DbValue `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"` + // Transaction handle from DbTxBeginResponse; 0 = no transaction + // (autocommit). + TxHandle uint64 `protobuf:"varint,3,opt,name=tx_handle,json=txHandle,proto3" json:"tx_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbQueryRequest) Reset() { + *x = DbQueryRequest{} + mi := &file_v1_db_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbQueryRequest) ProtoMessage() {} + +func (x *DbQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbQueryRequest.ProtoReflect.Descriptor instead. +func (*DbQueryRequest) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{5} +} + +func (x *DbQueryRequest) GetSql() string { + if x != nil { + return x.Sql + } + return "" +} + +func (x *DbQueryRequest) GetArgs() []*DbValue { + if x != nil { + return x.Args + } + return nil +} + +func (x *DbQueryRequest) GetTxHandle() uint64 { + if x != nil { + return x.TxHandle + } + return 0 +} + +// DbRowsResponse returns the full buffered result set. +type DbRowsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Columns []string `protobuf:"bytes,1,rep,name=columns,proto3" json:"columns,omitempty"` + Rows []*DbRow `protobuf:"bytes,2,rep,name=rows,proto3" json:"rows,omitempty"` + Error *DbError `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbRowsResponse) Reset() { + *x = DbRowsResponse{} + mi := &file_v1_db_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbRowsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbRowsResponse) ProtoMessage() {} + +func (x *DbRowsResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbRowsResponse.ProtoReflect.Descriptor instead. +func (*DbRowsResponse) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{6} +} + +func (x *DbRowsResponse) GetColumns() []string { + if x != nil { + return x.Columns + } + return nil +} + +func (x *DbRowsResponse) GetRows() []*DbRow { + if x != nil { + return x.Rows + } + return nil +} + +func (x *DbRowsResponse) GetError() *DbError { + if x != nil { + return x.Error + } + return nil +} + +// DbExecRequest executes a statement without returning rows. +type DbExecRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sql string `protobuf:"bytes,1,opt,name=sql,proto3" json:"sql,omitempty"` + Args []*DbValue `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"` + // Transaction handle from DbTxBeginResponse; 0 = no transaction. + TxHandle uint64 `protobuf:"varint,3,opt,name=tx_handle,json=txHandle,proto3" json:"tx_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbExecRequest) Reset() { + *x = DbExecRequest{} + mi := &file_v1_db_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbExecRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbExecRequest) ProtoMessage() {} + +func (x *DbExecRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbExecRequest.ProtoReflect.Descriptor instead. +func (*DbExecRequest) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{7} +} + +func (x *DbExecRequest) GetSql() string { + if x != nil { + return x.Sql + } + return "" +} + +func (x *DbExecRequest) GetArgs() []*DbValue { + if x != nil { + return x.Args + } + return nil +} + +func (x *DbExecRequest) GetTxHandle() uint64 { + if x != nil { + return x.TxHandle + } + return 0 +} + +type DbExecResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RowsAffected int64 `protobuf:"varint,1,opt,name=rows_affected,json=rowsAffected,proto3" json:"rows_affected,omitempty"` + Error *DbError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbExecResponse) Reset() { + *x = DbExecResponse{} + mi := &file_v1_db_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbExecResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbExecResponse) ProtoMessage() {} + +func (x *DbExecResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbExecResponse.ProtoReflect.Descriptor instead. +func (*DbExecResponse) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{8} +} + +func (x *DbExecResponse) GetRowsAffected() int64 { + if x != nil { + return x.RowsAffected + } + return 0 +} + +func (x *DbExecResponse) GetError() *DbError { + if x != nil { + return x.Error + } + return nil +} + +// DbTxBeginRequest opens a host-side transaction for this call chain. +type DbTxBeginRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbTxBeginRequest) Reset() { + *x = DbTxBeginRequest{} + mi := &file_v1_db_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbTxBeginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbTxBeginRequest) ProtoMessage() {} + +func (x *DbTxBeginRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbTxBeginRequest.ProtoReflect.Descriptor instead. +func (*DbTxBeginRequest) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{9} +} + +type DbTxBeginResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Opaque handle referencing the host-side transaction; never 0 on success. + TxHandle uint64 `protobuf:"varint,1,opt,name=tx_handle,json=txHandle,proto3" json:"tx_handle,omitempty"` + Error *DbError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbTxBeginResponse) Reset() { + *x = DbTxBeginResponse{} + mi := &file_v1_db_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbTxBeginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbTxBeginResponse) ProtoMessage() {} + +func (x *DbTxBeginResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbTxBeginResponse.ProtoReflect.Descriptor instead. +func (*DbTxBeginResponse) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{10} +} + +func (x *DbTxBeginResponse) GetTxHandle() uint64 { + if x != nil { + return x.TxHandle + } + return 0 +} + +func (x *DbTxBeginResponse) GetError() *DbError { + if x != nil { + return x.Error + } + return nil +} + +type DbTxCommitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TxHandle uint64 `protobuf:"varint,1,opt,name=tx_handle,json=txHandle,proto3" json:"tx_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbTxCommitRequest) Reset() { + *x = DbTxCommitRequest{} + mi := &file_v1_db_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbTxCommitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbTxCommitRequest) ProtoMessage() {} + +func (x *DbTxCommitRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbTxCommitRequest.ProtoReflect.Descriptor instead. +func (*DbTxCommitRequest) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{11} +} + +func (x *DbTxCommitRequest) GetTxHandle() uint64 { + if x != nil { + return x.TxHandle + } + return 0 +} + +type DbTxCommitResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Error *DbError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbTxCommitResponse) Reset() { + *x = DbTxCommitResponse{} + mi := &file_v1_db_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbTxCommitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbTxCommitResponse) ProtoMessage() {} + +func (x *DbTxCommitResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbTxCommitResponse.ProtoReflect.Descriptor instead. +func (*DbTxCommitResponse) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{12} +} + +func (x *DbTxCommitResponse) GetError() *DbError { + if x != nil { + return x.Error + } + return nil +} + +type DbTxRollbackRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TxHandle uint64 `protobuf:"varint,1,opt,name=tx_handle,json=txHandle,proto3" json:"tx_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbTxRollbackRequest) Reset() { + *x = DbTxRollbackRequest{} + mi := &file_v1_db_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbTxRollbackRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbTxRollbackRequest) ProtoMessage() {} + +func (x *DbTxRollbackRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbTxRollbackRequest.ProtoReflect.Descriptor instead. +func (*DbTxRollbackRequest) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{13} +} + +func (x *DbTxRollbackRequest) GetTxHandle() uint64 { + if x != nil { + return x.TxHandle + } + return 0 +} + +type DbTxRollbackResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Error *DbError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DbTxRollbackResponse) Reset() { + *x = DbTxRollbackResponse{} + mi := &file_v1_db_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DbTxRollbackResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DbTxRollbackResponse) ProtoMessage() {} + +func (x *DbTxRollbackResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_db_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DbTxRollbackResponse.ProtoReflect.Descriptor instead. +func (*DbTxRollbackResponse) Descriptor() ([]byte, []int) { + return file_v1_db_proto_rawDescGZIP(), []int{14} +} + +func (x *DbTxRollbackResponse) GetError() *DbError { + if x != nil { + return x.Error + } + return nil +} + +var File_v1_db_proto protoreflect.FileDescriptor + +const file_v1_db_proto_rawDesc = "" + + "\n" + + "\vv1/db.proto\x12\x06abi.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8a\x04\n" + + "\aDbValue\x12\x14\n" + + "\x04null\x18\x01 \x01(\bH\x00R\x04null\x12\x1f\n" + + "\n" + + "bool_value\x18\x02 \x01(\bH\x00R\tboolValue\x12!\n" + + "\vint64_value\x18\x03 \x01(\x03H\x00R\n" + + "int64Value\x12%\n" + + "\rfloat64_value\x18\x04 \x01(\x01H\x00R\ffloat64Value\x12#\n" + + "\fstring_value\x18\x05 \x01(\tH\x00R\vstringValue\x12!\n" + + "\vbytes_value\x18\x06 \x01(\fH\x00R\n" + + "bytesValue\x12E\n" + + "\x0ftimestamp_value\x18\a \x01(\v2\x1a.google.protobuf.TimestampH\x00R\x0etimestampValue\x12\x1f\n" + + "\n" + + "uuid_value\x18\b \x01(\tH\x00R\tuuidValue\x12!\n" + + "\vjsonb_value\x18\t \x01(\fH\x00R\n" + + "jsonbValue\x12%\n" + + "\rnumeric_value\x18\n" + + " \x01(\tH\x00R\fnumericValue\x12=\n" + + "\x10text_array_value\x18\v \x01(\v2\x11.abi.v1.TextArrayH\x00R\x0etextArrayValue\x12=\n" + + "\x10uuid_array_value\x18\f \x01(\v2\x11.abi.v1.UuidArrayH\x00R\x0euuidArrayValueB\x06\n" + + "\x04kind\"#\n" + + "\tTextArray\x12\x16\n" + + "\x06values\x18\x01 \x03(\tR\x06values\"#\n" + + "\tUuidArray\x12\x16\n" + + "\x06values\x18\x01 \x03(\tR\x06values\"0\n" + + "\x05DbRow\x12'\n" + + "\x06values\x18\x01 \x03(\v2\x0f.abi.v1.DbValueR\x06values\"7\n" + + "\aDbError\x12\x12\n" + + "\x04code\x18\x01 \x01(\tR\x04code\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"d\n" + + "\x0eDbQueryRequest\x12\x10\n" + + "\x03sql\x18\x01 \x01(\tR\x03sql\x12#\n" + + "\x04args\x18\x02 \x03(\v2\x0f.abi.v1.DbValueR\x04args\x12\x1b\n" + + "\ttx_handle\x18\x03 \x01(\x04R\btxHandle\"t\n" + + "\x0eDbRowsResponse\x12\x18\n" + + "\acolumns\x18\x01 \x03(\tR\acolumns\x12!\n" + + "\x04rows\x18\x02 \x03(\v2\r.abi.v1.DbRowR\x04rows\x12%\n" + + "\x05error\x18\x03 \x01(\v2\x0f.abi.v1.DbErrorR\x05error\"c\n" + + "\rDbExecRequest\x12\x10\n" + + "\x03sql\x18\x01 \x01(\tR\x03sql\x12#\n" + + "\x04args\x18\x02 \x03(\v2\x0f.abi.v1.DbValueR\x04args\x12\x1b\n" + + "\ttx_handle\x18\x03 \x01(\x04R\btxHandle\"\\\n" + + "\x0eDbExecResponse\x12#\n" + + "\rrows_affected\x18\x01 \x01(\x03R\frowsAffected\x12%\n" + + "\x05error\x18\x02 \x01(\v2\x0f.abi.v1.DbErrorR\x05error\"\x12\n" + + "\x10DbTxBeginRequest\"W\n" + + "\x11DbTxBeginResponse\x12\x1b\n" + + "\ttx_handle\x18\x01 \x01(\x04R\btxHandle\x12%\n" + + "\x05error\x18\x02 \x01(\v2\x0f.abi.v1.DbErrorR\x05error\"0\n" + + "\x11DbTxCommitRequest\x12\x1b\n" + + "\ttx_handle\x18\x01 \x01(\x04R\btxHandle\";\n" + + "\x12DbTxCommitResponse\x12%\n" + + "\x05error\x18\x01 \x01(\v2\x0f.abi.v1.DbErrorR\x05error\"2\n" + + "\x13DbTxRollbackRequest\x12\x1b\n" + + "\ttx_handle\x18\x01 \x01(\x04R\btxHandle\"=\n" + + "\x14DbTxRollbackResponse\x12%\n" + + "\x05error\x18\x01 \x01(\v2\x0f.abi.v1.DbErrorR\x05errorB5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3" + +var ( + file_v1_db_proto_rawDescOnce sync.Once + file_v1_db_proto_rawDescData []byte +) + +func file_v1_db_proto_rawDescGZIP() []byte { + file_v1_db_proto_rawDescOnce.Do(func() { + file_v1_db_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_db_proto_rawDesc), len(file_v1_db_proto_rawDesc))) + }) + return file_v1_db_proto_rawDescData +} + +var file_v1_db_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_v1_db_proto_goTypes = []any{ + (*DbValue)(nil), // 0: abi.v1.DbValue + (*TextArray)(nil), // 1: abi.v1.TextArray + (*UuidArray)(nil), // 2: abi.v1.UuidArray + (*DbRow)(nil), // 3: abi.v1.DbRow + (*DbError)(nil), // 4: abi.v1.DbError + (*DbQueryRequest)(nil), // 5: abi.v1.DbQueryRequest + (*DbRowsResponse)(nil), // 6: abi.v1.DbRowsResponse + (*DbExecRequest)(nil), // 7: abi.v1.DbExecRequest + (*DbExecResponse)(nil), // 8: abi.v1.DbExecResponse + (*DbTxBeginRequest)(nil), // 9: abi.v1.DbTxBeginRequest + (*DbTxBeginResponse)(nil), // 10: abi.v1.DbTxBeginResponse + (*DbTxCommitRequest)(nil), // 11: abi.v1.DbTxCommitRequest + (*DbTxCommitResponse)(nil), // 12: abi.v1.DbTxCommitResponse + (*DbTxRollbackRequest)(nil), // 13: abi.v1.DbTxRollbackRequest + (*DbTxRollbackResponse)(nil), // 14: abi.v1.DbTxRollbackResponse + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp +} +var file_v1_db_proto_depIdxs = []int32{ + 15, // 0: abi.v1.DbValue.timestamp_value:type_name -> google.protobuf.Timestamp + 1, // 1: abi.v1.DbValue.text_array_value:type_name -> abi.v1.TextArray + 2, // 2: abi.v1.DbValue.uuid_array_value:type_name -> abi.v1.UuidArray + 0, // 3: abi.v1.DbRow.values:type_name -> abi.v1.DbValue + 0, // 4: abi.v1.DbQueryRequest.args:type_name -> abi.v1.DbValue + 3, // 5: abi.v1.DbRowsResponse.rows:type_name -> abi.v1.DbRow + 4, // 6: abi.v1.DbRowsResponse.error:type_name -> abi.v1.DbError + 0, // 7: abi.v1.DbExecRequest.args:type_name -> abi.v1.DbValue + 4, // 8: abi.v1.DbExecResponse.error:type_name -> abi.v1.DbError + 4, // 9: abi.v1.DbTxBeginResponse.error:type_name -> abi.v1.DbError + 4, // 10: abi.v1.DbTxCommitResponse.error:type_name -> abi.v1.DbError + 4, // 11: abi.v1.DbTxRollbackResponse.error:type_name -> abi.v1.DbError + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name +} + +func init() { file_v1_db_proto_init() } +func file_v1_db_proto_init() { + if File_v1_db_proto != nil { + return + } + file_v1_db_proto_msgTypes[0].OneofWrappers = []any{ + (*DbValue_Null)(nil), + (*DbValue_BoolValue)(nil), + (*DbValue_Int64Value)(nil), + (*DbValue_Float64Value)(nil), + (*DbValue_StringValue)(nil), + (*DbValue_BytesValue)(nil), + (*DbValue_TimestampValue)(nil), + (*DbValue_UuidValue)(nil), + (*DbValue_JsonbValue)(nil), + (*DbValue_NumericValue)(nil), + (*DbValue_TextArrayValue)(nil), + (*DbValue_UuidArrayValue)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_db_proto_rawDesc), len(file_v1_db_proto_rawDesc)), + NumEnums: 0, + NumMessages: 15, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_db_proto_goTypes, + DependencyIndexes: file_v1_db_proto_depIdxs, + MessageInfos: file_v1_db_proto_msgTypes, + }.Build() + File_v1_db_proto = out.File + file_v1_db_proto_goTypes = nil + file_v1_db_proto_depIdxs = nil +} diff --git a/abi/v1/http.pb.go b/abi/v1/http.pb.go new file mode 100644 index 0000000..fe5c624 --- /dev/null +++ b/abi/v1/http.pb.go @@ -0,0 +1,296 @@ +// http.proto — buffered HTTP request/response payloads for the HANDLE_HTTP +// hook (WO-WZ-001). +// +// v1 buffers full bodies: no streaming, SSE, or WebSockets inside plugins +// (per the wasm migration design spec §5). The guest runs its real +// chi/connect mux internally and answers one HttpRequest with one +// HttpResponse. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: v1/http.proto + +package abiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// HttpRequest is a fully buffered HTTP request forwarded to the guest mux. +type HttpRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + // Request path (no scheme/host/query). + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + // Raw query string (without the leading '?'). + RawQuery string `protobuf:"bytes,3,opt,name=raw_query,json=rawQuery,proto3" json:"raw_query,omitempty"` + // Canonical header name → values. + Headers map[string]*HeaderValues `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpRequest) Reset() { + *x = HttpRequest{} + mi := &file_v1_http_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpRequest) ProtoMessage() {} + +func (x *HttpRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_http_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HttpRequest.ProtoReflect.Descriptor instead. +func (*HttpRequest) Descriptor() ([]byte, []int) { + return file_v1_http_proto_rawDescGZIP(), []int{0} +} + +func (x *HttpRequest) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *HttpRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *HttpRequest) GetRawQuery() string { + if x != nil { + return x.RawQuery + } + return "" +} + +func (x *HttpRequest) GetHeaders() map[string]*HeaderValues { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HttpRequest) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +// HeaderValues holds the values of one multi-valued HTTP header. +type HeaderValues struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeaderValues) Reset() { + *x = HeaderValues{} + mi := &file_v1_http_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeaderValues) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeaderValues) ProtoMessage() {} + +func (x *HeaderValues) ProtoReflect() protoreflect.Message { + mi := &file_v1_http_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeaderValues.ProtoReflect.Descriptor instead. +func (*HeaderValues) Descriptor() ([]byte, []int) { + return file_v1_http_proto_rawDescGZIP(), []int{1} +} + +func (x *HeaderValues) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +// HttpResponse is the guest's fully buffered response. +type HttpResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status int32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + Headers map[string]*HeaderValues `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HttpResponse) Reset() { + *x = HttpResponse{} + mi := &file_v1_http_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HttpResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpResponse) ProtoMessage() {} + +func (x *HttpResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_http_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HttpResponse.ProtoReflect.Descriptor instead. +func (*HttpResponse) Descriptor() ([]byte, []int) { + return file_v1_http_proto_rawDescGZIP(), []int{2} +} + +func (x *HttpResponse) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *HttpResponse) GetHeaders() map[string]*HeaderValues { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HttpResponse) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +var File_v1_http_proto protoreflect.FileDescriptor + +const file_v1_http_proto_rawDesc = "" + + "\n" + + "\rv1/http.proto\x12\x06abi.v1\"\xf8\x01\n" + + "\vHttpRequest\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x1b\n" + + "\traw_query\x18\x03 \x01(\tR\brawQuery\x12:\n" + + "\aheaders\x18\x04 \x03(\v2 .abi.v1.HttpRequest.HeadersEntryR\aheaders\x12\x12\n" + + "\x04body\x18\x05 \x01(\fR\x04body\x1aP\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12*\n" + + "\x05value\x18\x02 \x01(\v2\x14.abi.v1.HeaderValuesR\x05value:\x028\x01\"&\n" + + "\fHeaderValues\x12\x16\n" + + "\x06values\x18\x01 \x03(\tR\x06values\"\xc9\x01\n" + + "\fHttpResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\x05R\x06status\x12;\n" + + "\aheaders\x18\x02 \x03(\v2!.abi.v1.HttpResponse.HeadersEntryR\aheaders\x12\x12\n" + + "\x04body\x18\x03 \x01(\fR\x04body\x1aP\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12*\n" + + "\x05value\x18\x02 \x01(\v2\x14.abi.v1.HeaderValuesR\x05value:\x028\x01B5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3" + +var ( + file_v1_http_proto_rawDescOnce sync.Once + file_v1_http_proto_rawDescData []byte +) + +func file_v1_http_proto_rawDescGZIP() []byte { + file_v1_http_proto_rawDescOnce.Do(func() { + file_v1_http_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_http_proto_rawDesc), len(file_v1_http_proto_rawDesc))) + }) + return file_v1_http_proto_rawDescData +} + +var file_v1_http_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_v1_http_proto_goTypes = []any{ + (*HttpRequest)(nil), // 0: abi.v1.HttpRequest + (*HeaderValues)(nil), // 1: abi.v1.HeaderValues + (*HttpResponse)(nil), // 2: abi.v1.HttpResponse + nil, // 3: abi.v1.HttpRequest.HeadersEntry + nil, // 4: abi.v1.HttpResponse.HeadersEntry +} +var file_v1_http_proto_depIdxs = []int32{ + 3, // 0: abi.v1.HttpRequest.headers:type_name -> abi.v1.HttpRequest.HeadersEntry + 4, // 1: abi.v1.HttpResponse.headers:type_name -> abi.v1.HttpResponse.HeadersEntry + 1, // 2: abi.v1.HttpRequest.HeadersEntry.value:type_name -> abi.v1.HeaderValues + 1, // 3: abi.v1.HttpResponse.HeadersEntry.value:type_name -> abi.v1.HeaderValues + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_v1_http_proto_init() } +func file_v1_http_proto_init() { + if File_v1_http_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_http_proto_rawDesc), len(file_v1_http_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_http_proto_goTypes, + DependencyIndexes: file_v1_http_proto_depIdxs, + MessageInfos: file_v1_http_proto_msgTypes, + }.Build() + File_v1_http_proto = out.File + file_v1_http_proto_goTypes = nil + file_v1_http_proto_depIdxs = nil +} diff --git a/abi/v1/invoke.pb.go b/abi/v1/invoke.pb.go new file mode 100644 index 0000000..6f51b2a --- /dev/null +++ b/abi/v1/invoke.pb.go @@ -0,0 +1,1891 @@ +// invoke.proto — the bn_invoke envelope, hook catalog, and the job/lifecycle +// hook payloads (WO-WZ-001). +// +// Every host→guest call crosses the wasm boundary as one guest export: +// +// bn_invoke(hook_id, ptr, len) → packed(ptr, len) +// +// where (ptr, len) frames a serialized InvokeRequest and the packed return +// frames a serialized InvokeResponse. Hook-specific payloads (render.proto, +// http.proto, and the messages below) travel inside InvokeRequest.payload / +// InvokeResponse.payload. See core/docs/wasm-abi.md. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: v1/invoke.proto + +package abiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Hook identifies the guest entry point being invoked. +type Hook int32 + +const ( + Hook_HOOK_UNSPECIFIED Hook = 0 + // Render one block: payload = RenderBlockRequest / RenderBlockResponse. + Hook_HOOK_RENDER_BLOCK Hook = 1 + // Render one template: payload = RenderTemplateRequest / RenderTemplateResponse. + Hook_HOOK_RENDER_TEMPLATE Hook = 2 + // Forward a buffered HTTP request to the guest's mux: + // payload = HttpRequest / HttpResponse. + Hook_HOOK_HANDLE_HTTP Hook = 3 + // Run a background job handler: payload = JobRequest / JobResponse. + Hook_HOOK_JOB Hook = 4 + // Plugin load lifecycle: payload = LoadRequest / LoadResponse. + Hook_HOOK_LOAD Hook = 5 + // Plugin unload lifecycle: payload = UnloadRequest / UnloadResponse. + Hook_HOOK_UNLOAD Hook = 6 + // Re-fetch content for RAG re-indexing: + // payload = RagFetchRequest / RagFetchResponse. + Hook_HOOK_RAG_FETCH Hook = 7 + // Media lifecycle event delivery: payload = MediaHookRequest / MediaHookResponse. + Hook_HOOK_MEDIA_HOOK Hook = 8 + // Capture the static manifest at publish time: + // payload = DescribeRequest / DescribeResponse. + Hook_HOOK_DESCRIBE Hook = 9 + // Invoke a plugin-declared template tag (manifest.declared_tags) that the + // host engine hit while rendering a powered block: + // payload = RenderTagRequest / RenderTagResponse. + Hook_HOOK_RENDER_TAG Hook = 10 + // Apply a plugin-declared template filter (manifest.declared_filters): + // payload = ApplyFilterRequest / ApplyFilterResponse. + Hook_HOOK_APPLY_FILTER Hook = 11 + // Execute a guest-registered AI tool handler (ai.tools.register): + // payload = AiToolCallRequest / AiToolCallResponse. + Hook_HOOK_AI_TOOL_CALL Hook = 12 + // Invoke a method on a bridge service this plugin registered + // (bridge.register_service): payload = BridgeCallRequest / BridgeCallResponse. + Hook_HOOK_BRIDGE_CALL Hook = 13 + // Render one directory panel section (DirectoryExtensions.PanelSections): + // payload = DirectoryPanelSectionRequest / DirectoryPanelSectionResponse. + Hook_HOOK_DIRECTORY_PANEL_SECTION Hook = 14 + // Run one directory pin decorator (DirectoryExtensions.PinDecorators): + // payload = DirectoryPinDecoratorRequest / DirectoryPinDecoratorResponse. + Hook_HOOK_DIRECTORY_PIN_DECORATOR Hook = 15 +) + +// Enum value maps for Hook. +var ( + Hook_name = map[int32]string{ + 0: "HOOK_UNSPECIFIED", + 1: "HOOK_RENDER_BLOCK", + 2: "HOOK_RENDER_TEMPLATE", + 3: "HOOK_HANDLE_HTTP", + 4: "HOOK_JOB", + 5: "HOOK_LOAD", + 6: "HOOK_UNLOAD", + 7: "HOOK_RAG_FETCH", + 8: "HOOK_MEDIA_HOOK", + 9: "HOOK_DESCRIBE", + 10: "HOOK_RENDER_TAG", + 11: "HOOK_APPLY_FILTER", + 12: "HOOK_AI_TOOL_CALL", + 13: "HOOK_BRIDGE_CALL", + 14: "HOOK_DIRECTORY_PANEL_SECTION", + 15: "HOOK_DIRECTORY_PIN_DECORATOR", + } + Hook_value = map[string]int32{ + "HOOK_UNSPECIFIED": 0, + "HOOK_RENDER_BLOCK": 1, + "HOOK_RENDER_TEMPLATE": 2, + "HOOK_HANDLE_HTTP": 3, + "HOOK_JOB": 4, + "HOOK_LOAD": 5, + "HOOK_UNLOAD": 6, + "HOOK_RAG_FETCH": 7, + "HOOK_MEDIA_HOOK": 8, + "HOOK_DESCRIBE": 9, + "HOOK_RENDER_TAG": 10, + "HOOK_APPLY_FILTER": 11, + "HOOK_AI_TOOL_CALL": 12, + "HOOK_BRIDGE_CALL": 13, + "HOOK_DIRECTORY_PANEL_SECTION": 14, + "HOOK_DIRECTORY_PIN_DECORATOR": 15, + } +) + +func (x Hook) Enum() *Hook { + p := new(Hook) + *p = x + return p +} + +func (x Hook) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Hook) Descriptor() protoreflect.EnumDescriptor { + return file_v1_invoke_proto_enumTypes[0].Descriptor() +} + +func (Hook) Type() protoreflect.EnumType { + return &file_v1_invoke_proto_enumTypes[0] +} + +func (x Hook) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Hook.Descriptor instead. +func (Hook) EnumDescriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{0} +} + +// AbiErrorCode classifies boundary-crossing failures. +type AbiErrorCode int32 + +const ( + AbiErrorCode_ABI_ERROR_CODE_UNSPECIFIED AbiErrorCode = 0 + // The handler ran and failed; message carries the Go error text. + AbiErrorCode_ABI_ERROR_CODE_INTERNAL AbiErrorCode = 1 + // The payload could not be decoded. + AbiErrorCode_ABI_ERROR_CODE_DECODE AbiErrorCode = 2 + // The hook/capability is not implemented by the callee. + AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED AbiErrorCode = 3 + // The call exceeded its deadline; the instance is considered poisoned. + AbiErrorCode_ABI_ERROR_CODE_DEADLINE_EXCEEDED AbiErrorCode = 4 + // The caller is not entitled to this capability. + AbiErrorCode_ABI_ERROR_CODE_PERMISSION_DENIED AbiErrorCode = 5 + // A db.* call named a transaction handle the host has already expired + // (dropped at the call-chain deadline, WO-WZ-007). Distinct from a real + // fault: the unit of work is retryable in a fresh transaction. Emitted by + // the cms dbexec side (adopted separately) and mapped guest-side to + // bnwasm.ErrTxExpired. + AbiErrorCode_ABI_ERROR_CODE_TX_EXPIRED AbiErrorCode = 6 +) + +// Enum value maps for AbiErrorCode. +var ( + AbiErrorCode_name = map[int32]string{ + 0: "ABI_ERROR_CODE_UNSPECIFIED", + 1: "ABI_ERROR_CODE_INTERNAL", + 2: "ABI_ERROR_CODE_DECODE", + 3: "ABI_ERROR_CODE_UNIMPLEMENTED", + 4: "ABI_ERROR_CODE_DEADLINE_EXCEEDED", + 5: "ABI_ERROR_CODE_PERMISSION_DENIED", + 6: "ABI_ERROR_CODE_TX_EXPIRED", + } + AbiErrorCode_value = map[string]int32{ + "ABI_ERROR_CODE_UNSPECIFIED": 0, + "ABI_ERROR_CODE_INTERNAL": 1, + "ABI_ERROR_CODE_DECODE": 2, + "ABI_ERROR_CODE_UNIMPLEMENTED": 3, + "ABI_ERROR_CODE_DEADLINE_EXCEEDED": 4, + "ABI_ERROR_CODE_PERMISSION_DENIED": 5, + "ABI_ERROR_CODE_TX_EXPIRED": 6, + } +) + +func (x AbiErrorCode) Enum() *AbiErrorCode { + p := new(AbiErrorCode) + *p = x + return p +} + +func (x AbiErrorCode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AbiErrorCode) Descriptor() protoreflect.EnumDescriptor { + return file_v1_invoke_proto_enumTypes[1].Descriptor() +} + +func (AbiErrorCode) Type() protoreflect.EnumType { + return &file_v1_invoke_proto_enumTypes[1] +} + +func (x AbiErrorCode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AbiErrorCode.Descriptor instead. +func (AbiErrorCode) EnumDescriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{1} +} + +// InvokeRequest is the host→guest call envelope. +type InvokeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hook Hook `protobuf:"varint,1,opt,name=hook,proto3,enum=abi.v1.Hook" json:"hook,omitempty"` + // Serialized hook-specific request message (see Hook value comments). + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` + // Milliseconds the guest has to answer; the host also enforces this + // deadline on the wasm instance (WithCloseOnContextDone). + DeadlineMs int64 `protobuf:"varint,3,opt,name=deadline_ms,json=deadlineMs,proto3" json:"deadline_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InvokeRequest) Reset() { + *x = InvokeRequest{} + mi := &file_v1_invoke_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InvokeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvokeRequest) ProtoMessage() {} + +func (x *InvokeRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvokeRequest.ProtoReflect.Descriptor instead. +func (*InvokeRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{0} +} + +func (x *InvokeRequest) GetHook() Hook { + if x != nil { + return x.Hook + } + return Hook_HOOK_UNSPECIFIED +} + +func (x *InvokeRequest) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *InvokeRequest) GetDeadlineMs() int64 { + if x != nil { + return x.DeadlineMs + } + return 0 +} + +// InvokeResponse is the guest→host return envelope. +type InvokeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Serialized hook-specific response message; empty when error is set. + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + // Set when the hook failed; the host treats decode failures and traps as + // implicit ABI_ERROR_CODE_INTERNAL. + Error *AbiError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InvokeResponse) Reset() { + *x = InvokeResponse{} + mi := &file_v1_invoke_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InvokeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvokeResponse) ProtoMessage() {} + +func (x *InvokeResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvokeResponse.ProtoReflect.Descriptor instead. +func (*InvokeResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{1} +} + +func (x *InvokeResponse) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *InvokeResponse) GetError() *AbiError { + if x != nil { + return x.Error + } + return nil +} + +// AbiError is the structured error carried by InvokeResponse and +// HostCallResponse. +type AbiError struct { + state protoimpl.MessageState `protogen:"open.v1"` + Code AbiErrorCode `protobuf:"varint,1,opt,name=code,proto3,enum=abi.v1.AbiErrorCode" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AbiError) Reset() { + *x = AbiError{} + mi := &file_v1_invoke_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AbiError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AbiError) ProtoMessage() {} + +func (x *AbiError) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AbiError.ProtoReflect.Descriptor instead. +func (*AbiError) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{2} +} + +func (x *AbiError) GetCode() AbiErrorCode { + if x != nil { + return x.Code + } + return AbiErrorCode_ABI_ERROR_CODE_UNSPECIFIED +} + +func (x *AbiError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// DescribeRequest asks the guest for its static manifest (publish time only). +type DescribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ABI major version of the calling host/publish tool. + HostAbiVersion uint32 `protobuf:"varint,1,opt,name=host_abi_version,json=hostAbiVersion,proto3" json:"host_abi_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeRequest) Reset() { + *x = DescribeRequest{} + mi := &file_v1_invoke_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeRequest) ProtoMessage() {} + +func (x *DescribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeRequest.ProtoReflect.Descriptor instead. +func (*DescribeRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{3} +} + +func (x *DescribeRequest) GetHostAbiVersion() uint32 { + if x != nil { + return x.HostAbiVersion + } + return 0 +} + +// DescribeResponse returns the static manifest. +type DescribeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Manifest *PluginManifest `protobuf:"bytes,1,opt,name=manifest,proto3" json:"manifest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeResponse) Reset() { + *x = DescribeResponse{} + mi := &file_v1_invoke_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeResponse) ProtoMessage() {} + +func (x *DescribeResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeResponse.ProtoReflect.Descriptor instead. +func (*DescribeResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{4} +} + +func (x *DescribeResponse) GetManifest() *PluginManifest { + if x != nil { + return x.Manifest + } + return nil +} + +// LoadRequest carries the static host configuration the .so world exposed as +// CoreServices.AppURL / CoreServices.MediaPath. +type LoadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + HostConfig *HostConfig `protobuf:"bytes,1,opt,name=host_config,json=hostConfig,proto3" json:"host_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoadRequest) Reset() { + *x = LoadRequest{} + mi := &file_v1_invoke_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoadRequest) ProtoMessage() {} + +func (x *LoadRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoadRequest.ProtoReflect.Descriptor instead. +func (*LoadRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{5} +} + +func (x *LoadRequest) GetHostConfig() *HostConfig { + if x != nil { + return x.HostConfig + } + return nil +} + +// HostConfig mirrors the plain-value CoreServices fields. +type HostConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + AppUrl string `protobuf:"bytes,1,opt,name=app_url,json=appUrl,proto3" json:"app_url,omitempty"` // CoreServices.AppURL + MediaPath string `protobuf:"bytes,2,opt,name=media_path,json=mediaPath,proto3" json:"media_path,omitempty"` // CoreServices.MediaPath + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostConfig) Reset() { + *x = HostConfig{} + mi := &file_v1_invoke_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostConfig) ProtoMessage() {} + +func (x *HostConfig) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostConfig.ProtoReflect.Descriptor instead. +func (*HostConfig) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{6} +} + +func (x *HostConfig) GetAppUrl() string { + if x != nil { + return x.AppUrl + } + return "" +} + +func (x *HostConfig) GetMediaPath() string { + if x != nil { + return x.MediaPath + } + return "" +} + +type LoadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoadResponse) Reset() { + *x = LoadResponse{} + mi := &file_v1_invoke_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoadResponse) ProtoMessage() {} + +func (x *LoadResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoadResponse.ProtoReflect.Descriptor instead. +func (*LoadResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{7} +} + +type UnloadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnloadRequest) Reset() { + *x = UnloadRequest{} + mi := &file_v1_invoke_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnloadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnloadRequest) ProtoMessage() {} + +func (x *UnloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnloadRequest.ProtoReflect.Descriptor instead. +func (*UnloadRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{8} +} + +type UnloadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnloadResponse) Reset() { + *x = UnloadResponse{} + mi := &file_v1_invoke_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnloadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnloadResponse) ProtoMessage() {} + +func (x *UnloadResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnloadResponse.ProtoReflect.Descriptor instead. +func (*UnloadResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{9} +} + +// JobRequest dispatches one background job to the guest handler registered +// for job_type (manifest.job_types). +type JobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + JobType string `protobuf:"bytes,1,opt,name=job_type,json=jobType,proto3" json:"job_type,omitempty"` + // JSON job configuration (json.RawMessage in JobHandlerFunc). + ConfigJson []byte `protobuf:"bytes,2,opt,name=config_json,json=configJson,proto3" json:"config_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobRequest) Reset() { + *x = JobRequest{} + mi := &file_v1_invoke_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobRequest) ProtoMessage() {} + +func (x *JobRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobRequest.ProtoReflect.Descriptor instead. +func (*JobRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{10} +} + +func (x *JobRequest) GetJobType() string { + if x != nil { + return x.JobType + } + return "" +} + +func (x *JobRequest) GetConfigJson() []byte { + if x != nil { + return x.ConfigJson + } + return nil +} + +// JobResponse returns the handler's JSON result. +type JobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResultJson []byte `protobuf:"bytes,1,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *JobResponse) Reset() { + *x = JobResponse{} + mi := &file_v1_invoke_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *JobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JobResponse) ProtoMessage() {} + +func (x *JobResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use JobResponse.ProtoReflect.Descriptor instead. +func (*JobResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{11} +} + +func (x *JobResponse) GetResultJson() []byte { + if x != nil { + return x.ResultJson + } + return nil +} + +// RagFetchRequest asks the guest's registered content fetcher +// (manifest.rag_content_fetcher_types) for a content item's text. +type RagFetchRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContentType string `protobuf:"bytes,1,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + ContentId string `protobuf:"bytes,2,opt,name=content_id,json=contentId,proto3" json:"content_id,omitempty"` // UUID + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RagFetchRequest) Reset() { + *x = RagFetchRequest{} + mi := &file_v1_invoke_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RagFetchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RagFetchRequest) ProtoMessage() {} + +func (x *RagFetchRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RagFetchRequest.ProtoReflect.Descriptor instead. +func (*RagFetchRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{12} +} + +func (x *RagFetchRequest) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *RagFetchRequest) GetContentId() string { + if x != nil { + return x.ContentId + } + return "" +} + +// RagFetchResponse mirrors plugin.ContentFetcher's return values. +type RagFetchResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RagFetchResponse) Reset() { + *x = RagFetchResponse{} + mi := &file_v1_invoke_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RagFetchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RagFetchResponse) ProtoMessage() {} + +func (x *RagFetchResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RagFetchResponse.ProtoReflect.Descriptor instead. +func (*RagFetchResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{13} +} + +func (x *RagFetchResponse) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *RagFetchResponse) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +// MediaHookRequest delivers one media lifecycle event +// (plugin.MediaHooksProvider). +type MediaHookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *MediaHookRequest_MediaAnalyzed + // *MediaHookRequest_ModerationDecision + Event isMediaHookRequest_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MediaHookRequest) Reset() { + *x = MediaHookRequest{} + mi := &file_v1_invoke_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MediaHookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MediaHookRequest) ProtoMessage() {} + +func (x *MediaHookRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MediaHookRequest.ProtoReflect.Descriptor instead. +func (*MediaHookRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{14} +} + +func (x *MediaHookRequest) GetEvent() isMediaHookRequest_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *MediaHookRequest) GetMediaAnalyzed() *MediaAnalyzedEvent { + if x != nil { + if x, ok := x.Event.(*MediaHookRequest_MediaAnalyzed); ok { + return x.MediaAnalyzed + } + } + return nil +} + +func (x *MediaHookRequest) GetModerationDecision() *ModerationDecisionEvent { + if x != nil { + if x, ok := x.Event.(*MediaHookRequest_ModerationDecision); ok { + return x.ModerationDecision + } + } + return nil +} + +type isMediaHookRequest_Event interface { + isMediaHookRequest_Event() +} + +type MediaHookRequest_MediaAnalyzed struct { + MediaAnalyzed *MediaAnalyzedEvent `protobuf:"bytes,1,opt,name=media_analyzed,json=mediaAnalyzed,proto3,oneof"` // OnMediaAnalyzed +} + +type MediaHookRequest_ModerationDecision struct { + ModerationDecision *ModerationDecisionEvent `protobuf:"bytes,2,opt,name=moderation_decision,json=moderationDecision,proto3,oneof"` // OnModerationDecision +} + +func (*MediaHookRequest_MediaAnalyzed) isMediaHookRequest_Event() {} + +func (*MediaHookRequest_ModerationDecision) isMediaHookRequest_Event() {} + +type MediaHookResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MediaHookResponse) Reset() { + *x = MediaHookResponse{} + mi := &file_v1_invoke_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MediaHookResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MediaHookResponse) ProtoMessage() {} + +func (x *MediaHookResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MediaHookResponse.ProtoReflect.Descriptor instead. +func (*MediaHookResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{15} +} + +// MediaAnalyzedEvent mirrors plugin.MediaAnalyzedEvent (UUIDs as strings). +type MediaAnalyzedEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + MediaId string `protobuf:"bytes,1,opt,name=media_id,json=mediaId,proto3" json:"media_id,omitempty"` + AnalysisId string `protobuf:"bytes,2,opt,name=analysis_id,json=analysisId,proto3" json:"analysis_id,omitempty"` + ContentHash string `protobuf:"bytes,3,opt,name=content_hash,json=contentHash,proto3" json:"content_hash,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + SourcePlugin string `protobuf:"bytes,5,opt,name=source_plugin,json=sourcePlugin,proto3" json:"source_plugin,omitempty"` + SourceType string `protobuf:"bytes,6,opt,name=source_type,json=sourceType,proto3" json:"source_type,omitempty"` + SourceRefId string `protobuf:"bytes,7,opt,name=source_ref_id,json=sourceRefId,proto3" json:"source_ref_id,omitempty"` + SafeAdult string `protobuf:"bytes,8,opt,name=safe_adult,json=safeAdult,proto3" json:"safe_adult,omitempty"` + SafeViolence string `protobuf:"bytes,9,opt,name=safe_violence,json=safeViolence,proto3" json:"safe_violence,omitempty"` + SafeRacy string `protobuf:"bytes,10,opt,name=safe_racy,json=safeRacy,proto3" json:"safe_racy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MediaAnalyzedEvent) Reset() { + *x = MediaAnalyzedEvent{} + mi := &file_v1_invoke_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MediaAnalyzedEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MediaAnalyzedEvent) ProtoMessage() {} + +func (x *MediaAnalyzedEvent) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MediaAnalyzedEvent.ProtoReflect.Descriptor instead. +func (*MediaAnalyzedEvent) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{16} +} + +func (x *MediaAnalyzedEvent) GetMediaId() string { + if x != nil { + return x.MediaId + } + return "" +} + +func (x *MediaAnalyzedEvent) GetAnalysisId() string { + if x != nil { + return x.AnalysisId + } + return "" +} + +func (x *MediaAnalyzedEvent) GetContentHash() string { + if x != nil { + return x.ContentHash + } + return "" +} + +func (x *MediaAnalyzedEvent) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *MediaAnalyzedEvent) GetSourcePlugin() string { + if x != nil { + return x.SourcePlugin + } + return "" +} + +func (x *MediaAnalyzedEvent) GetSourceType() string { + if x != nil { + return x.SourceType + } + return "" +} + +func (x *MediaAnalyzedEvent) GetSourceRefId() string { + if x != nil { + return x.SourceRefId + } + return "" +} + +func (x *MediaAnalyzedEvent) GetSafeAdult() string { + if x != nil { + return x.SafeAdult + } + return "" +} + +func (x *MediaAnalyzedEvent) GetSafeViolence() string { + if x != nil { + return x.SafeViolence + } + return "" +} + +func (x *MediaAnalyzedEvent) GetSafeRacy() string { + if x != nil { + return x.SafeRacy + } + return "" +} + +// AiToolCallRequest executes the guest-side Handler of a tool the plugin +// registered via ai.tools.register (ai.ToolHandler). +type AiToolCallRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` + // JSON encoding of the params map. + ParamsJson []byte `protobuf:"bytes,2,opt,name=params_json,json=paramsJson,proto3" json:"params_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AiToolCallRequest) Reset() { + *x = AiToolCallRequest{} + mi := &file_v1_invoke_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AiToolCallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AiToolCallRequest) ProtoMessage() {} + +func (x *AiToolCallRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AiToolCallRequest.ProtoReflect.Descriptor instead. +func (*AiToolCallRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{17} +} + +func (x *AiToolCallRequest) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *AiToolCallRequest) GetParamsJson() []byte { + if x != nil { + return x.ParamsJson + } + return nil +} + +// AiToolCallResponse mirrors ai.ToolResult. A handler-level failure travels +// in error_message (a normal tool outcome the model sees); AbiError stays +// reserved for transport/decode/panic failures. +type AiToolCallResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AiToolCallResponse) Reset() { + *x = AiToolCallResponse{} + mi := &file_v1_invoke_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AiToolCallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AiToolCallResponse) ProtoMessage() {} + +func (x *AiToolCallResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AiToolCallResponse.ProtoReflect.Descriptor instead. +func (*AiToolCallResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{18} +} + +func (x *AiToolCallResponse) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *AiToolCallResponse) GetErrorMessage() string { + if x != nil { + return x.ErrorMessage + } + return "" +} + +// BridgeCallRequest asks THIS plugin (the provider) to run a method on one of +// its registered bridge services. The consumer side is the bridge.invoke +// capability; payload encoding is agreed between the two plugins (JSON by +// convention). +type BridgeCallRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"` + Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeCallRequest) Reset() { + *x = BridgeCallRequest{} + mi := &file_v1_invoke_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeCallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeCallRequest) ProtoMessage() {} + +func (x *BridgeCallRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeCallRequest.ProtoReflect.Descriptor instead. +func (*BridgeCallRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{19} +} + +func (x *BridgeCallRequest) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *BridgeCallRequest) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *BridgeCallRequest) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +type BridgeCallResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BridgeCallResponse) Reset() { + *x = BridgeCallResponse{} + mi := &file_v1_invoke_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BridgeCallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BridgeCallResponse) ProtoMessage() {} + +func (x *BridgeCallResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BridgeCallResponse.ProtoReflect.Descriptor instead. +func (*BridgeCallResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{20} +} + +func (x *BridgeCallResponse) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +// DirectoryPanelSectionRequest renders the index-th registered panel section +// (DirectoryExtensions.PanelSections[index], counted in +// manifest.directory_extensions.panel_section_count). +type DirectoryPanelSectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Index uint32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` + // JSON encoding of the row data map. + DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectoryPanelSectionRequest) Reset() { + *x = DirectoryPanelSectionRequest{} + mi := &file_v1_invoke_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectoryPanelSectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectoryPanelSectionRequest) ProtoMessage() {} + +func (x *DirectoryPanelSectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectoryPanelSectionRequest.ProtoReflect.Descriptor instead. +func (*DirectoryPanelSectionRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{21} +} + +func (x *DirectoryPanelSectionRequest) GetIndex() uint32 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *DirectoryPanelSectionRequest) GetDataJson() []byte { + if x != nil { + return x.DataJson + } + return nil +} + +type DirectoryPanelSectionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Html string `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectoryPanelSectionResponse) Reset() { + *x = DirectoryPanelSectionResponse{} + mi := &file_v1_invoke_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectoryPanelSectionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectoryPanelSectionResponse) ProtoMessage() {} + +func (x *DirectoryPanelSectionResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectoryPanelSectionResponse.ProtoReflect.Descriptor instead. +func (*DirectoryPanelSectionResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{22} +} + +func (x *DirectoryPanelSectionResponse) GetHtml() string { + if x != nil { + return x.Html + } + return "" +} + +// DirectoryPinDecoratorRequest runs the index-th registered pin decorator, +// which mutates the pin map in place; the mutated pin is returned. +type DirectoryPinDecoratorRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Index uint32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` + // JSON encoding of the pin map (mutated by the decorator). + PinJson []byte `protobuf:"bytes,2,opt,name=pin_json,json=pinJson,proto3" json:"pin_json,omitempty"` + // JSON encoding of the row data map (read-only input). + DataJson []byte `protobuf:"bytes,3,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectoryPinDecoratorRequest) Reset() { + *x = DirectoryPinDecoratorRequest{} + mi := &file_v1_invoke_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectoryPinDecoratorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectoryPinDecoratorRequest) ProtoMessage() {} + +func (x *DirectoryPinDecoratorRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectoryPinDecoratorRequest.ProtoReflect.Descriptor instead. +func (*DirectoryPinDecoratorRequest) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{23} +} + +func (x *DirectoryPinDecoratorRequest) GetIndex() uint32 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *DirectoryPinDecoratorRequest) GetPinJson() []byte { + if x != nil { + return x.PinJson + } + return nil +} + +func (x *DirectoryPinDecoratorRequest) GetDataJson() []byte { + if x != nil { + return x.DataJson + } + return nil +} + +type DirectoryPinDecoratorResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // JSON encoding of the mutated pin map. + PinJson []byte `protobuf:"bytes,1,opt,name=pin_json,json=pinJson,proto3" json:"pin_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectoryPinDecoratorResponse) Reset() { + *x = DirectoryPinDecoratorResponse{} + mi := &file_v1_invoke_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectoryPinDecoratorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectoryPinDecoratorResponse) ProtoMessage() {} + +func (x *DirectoryPinDecoratorResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectoryPinDecoratorResponse.ProtoReflect.Descriptor instead. +func (*DirectoryPinDecoratorResponse) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{24} +} + +func (x *DirectoryPinDecoratorResponse) GetPinJson() []byte { + if x != nil { + return x.PinJson + } + return nil +} + +// ModerationDecisionEvent mirrors plugin.ModerationDecisionEvent. +type ModerationDecisionEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + MediaId string `protobuf:"bytes,1,opt,name=media_id,json=mediaId,proto3" json:"media_id,omitempty"` + AnalysisId string `protobuf:"bytes,2,opt,name=analysis_id,json=analysisId,proto3" json:"analysis_id,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + PreviousStatus string `protobuf:"bytes,4,opt,name=previous_status,json=previousStatus,proto3" json:"previous_status,omitempty"` + SourcePlugin string `protobuf:"bytes,5,opt,name=source_plugin,json=sourcePlugin,proto3" json:"source_plugin,omitempty"` + SourceType string `protobuf:"bytes,6,opt,name=source_type,json=sourceType,proto3" json:"source_type,omitempty"` + SourceRefId string `protobuf:"bytes,7,opt,name=source_ref_id,json=sourceRefId,proto3" json:"source_ref_id,omitempty"` + ModeratedBy string `protobuf:"bytes,8,opt,name=moderated_by,json=moderatedBy,proto3" json:"moderated_by,omitempty"` + Note string `protobuf:"bytes,9,opt,name=note,proto3" json:"note,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModerationDecisionEvent) Reset() { + *x = ModerationDecisionEvent{} + mi := &file_v1_invoke_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModerationDecisionEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModerationDecisionEvent) ProtoMessage() {} + +func (x *ModerationDecisionEvent) ProtoReflect() protoreflect.Message { + mi := &file_v1_invoke_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModerationDecisionEvent.ProtoReflect.Descriptor instead. +func (*ModerationDecisionEvent) Descriptor() ([]byte, []int) { + return file_v1_invoke_proto_rawDescGZIP(), []int{25} +} + +func (x *ModerationDecisionEvent) GetMediaId() string { + if x != nil { + return x.MediaId + } + return "" +} + +func (x *ModerationDecisionEvent) GetAnalysisId() string { + if x != nil { + return x.AnalysisId + } + return "" +} + +func (x *ModerationDecisionEvent) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ModerationDecisionEvent) GetPreviousStatus() string { + if x != nil { + return x.PreviousStatus + } + return "" +} + +func (x *ModerationDecisionEvent) GetSourcePlugin() string { + if x != nil { + return x.SourcePlugin + } + return "" +} + +func (x *ModerationDecisionEvent) GetSourceType() string { + if x != nil { + return x.SourceType + } + return "" +} + +func (x *ModerationDecisionEvent) GetSourceRefId() string { + if x != nil { + return x.SourceRefId + } + return "" +} + +func (x *ModerationDecisionEvent) GetModeratedBy() string { + if x != nil { + return x.ModeratedBy + } + return "" +} + +func (x *ModerationDecisionEvent) GetNote() string { + if x != nil { + return x.Note + } + return "" +} + +var File_v1_invoke_proto protoreflect.FileDescriptor + +const file_v1_invoke_proto_rawDesc = "" + + "\n" + + "\x0fv1/invoke.proto\x12\x06abi.v1\x1a\x11v1/manifest.proto\"l\n" + + "\rInvokeRequest\x12 \n" + + "\x04hook\x18\x01 \x01(\x0e2\f.abi.v1.HookR\x04hook\x12\x18\n" + + "\apayload\x18\x02 \x01(\fR\apayload\x12\x1f\n" + + "\vdeadline_ms\x18\x03 \x01(\x03R\n" + + "deadlineMs\"R\n" + + "\x0eInvokeResponse\x12\x18\n" + + "\apayload\x18\x01 \x01(\fR\apayload\x12&\n" + + "\x05error\x18\x02 \x01(\v2\x10.abi.v1.AbiErrorR\x05error\"N\n" + + "\bAbiError\x12(\n" + + "\x04code\x18\x01 \x01(\x0e2\x14.abi.v1.AbiErrorCodeR\x04code\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\";\n" + + "\x0fDescribeRequest\x12(\n" + + "\x10host_abi_version\x18\x01 \x01(\rR\x0ehostAbiVersion\"F\n" + + "\x10DescribeResponse\x122\n" + + "\bmanifest\x18\x01 \x01(\v2\x16.abi.v1.PluginManifestR\bmanifest\"B\n" + + "\vLoadRequest\x123\n" + + "\vhost_config\x18\x01 \x01(\v2\x12.abi.v1.HostConfigR\n" + + "hostConfig\"D\n" + + "\n" + + "HostConfig\x12\x17\n" + + "\aapp_url\x18\x01 \x01(\tR\x06appUrl\x12\x1d\n" + + "\n" + + "media_path\x18\x02 \x01(\tR\tmediaPath\"\x0e\n" + + "\fLoadResponse\"\x0f\n" + + "\rUnloadRequest\"\x10\n" + + "\x0eUnloadResponse\"H\n" + + "\n" + + "JobRequest\x12\x19\n" + + "\bjob_type\x18\x01 \x01(\tR\ajobType\x12\x1f\n" + + "\vconfig_json\x18\x02 \x01(\fR\n" + + "configJson\".\n" + + "\vJobResponse\x12\x1f\n" + + "\vresult_json\x18\x01 \x01(\fR\n" + + "resultJson\"S\n" + + "\x0fRagFetchRequest\x12!\n" + + "\fcontent_type\x18\x01 \x01(\tR\vcontentType\x12\x1d\n" + + "\n" + + "content_id\x18\x02 \x01(\tR\tcontentId\"<\n" + + "\x10RagFetchResponse\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12\x12\n" + + "\x04text\x18\x02 \x01(\tR\x04text\"\xb4\x01\n" + + "\x10MediaHookRequest\x12C\n" + + "\x0emedia_analyzed\x18\x01 \x01(\v2\x1a.abi.v1.MediaAnalyzedEventH\x00R\rmediaAnalyzed\x12R\n" + + "\x13moderation_decision\x18\x02 \x01(\v2\x1f.abi.v1.ModerationDecisionEventH\x00R\x12moderationDecisionB\a\n" + + "\x05event\"\x13\n" + + "\x11MediaHookResponse\"\xd6\x02\n" + + "\x12MediaAnalyzedEvent\x12\x19\n" + + "\bmedia_id\x18\x01 \x01(\tR\amediaId\x12\x1f\n" + + "\vanalysis_id\x18\x02 \x01(\tR\n" + + "analysisId\x12!\n" + + "\fcontent_hash\x18\x03 \x01(\tR\vcontentHash\x12\x16\n" + + "\x06status\x18\x04 \x01(\tR\x06status\x12#\n" + + "\rsource_plugin\x18\x05 \x01(\tR\fsourcePlugin\x12\x1f\n" + + "\vsource_type\x18\x06 \x01(\tR\n" + + "sourceType\x12\"\n" + + "\rsource_ref_id\x18\a \x01(\tR\vsourceRefId\x12\x1d\n" + + "\n" + + "safe_adult\x18\b \x01(\tR\tsafeAdult\x12#\n" + + "\rsafe_violence\x18\t \x01(\tR\fsafeViolence\x12\x1b\n" + + "\tsafe_racy\x18\n" + + " \x01(\tR\bsafeRacy\"H\n" + + "\x11AiToolCallRequest\x12\x12\n" + + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" + + "\vparams_json\x18\x02 \x01(\fR\n" + + "paramsJson\"S\n" + + "\x12AiToolCallResponse\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\x12#\n" + + "\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"h\n" + + "\x11BridgeCallRequest\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x16\n" + + "\x06method\x18\x02 \x01(\tR\x06method\x12\x18\n" + + "\apayload\x18\x03 \x01(\fR\apayload\".\n" + + "\x12BridgeCallResponse\x12\x18\n" + + "\apayload\x18\x01 \x01(\fR\apayload\"Q\n" + + "\x1cDirectoryPanelSectionRequest\x12\x14\n" + + "\x05index\x18\x01 \x01(\rR\x05index\x12\x1b\n" + + "\tdata_json\x18\x02 \x01(\fR\bdataJson\"3\n" + + "\x1dDirectoryPanelSectionResponse\x12\x12\n" + + "\x04html\x18\x01 \x01(\tR\x04html\"l\n" + + "\x1cDirectoryPinDecoratorRequest\x12\x14\n" + + "\x05index\x18\x01 \x01(\rR\x05index\x12\x19\n" + + "\bpin_json\x18\x02 \x01(\fR\apinJson\x12\x1b\n" + + "\tdata_json\x18\x03 \x01(\fR\bdataJson\":\n" + + "\x1dDirectoryPinDecoratorResponse\x12\x19\n" + + "\bpin_json\x18\x01 \x01(\fR\apinJson\"\xb7\x02\n" + + "\x17ModerationDecisionEvent\x12\x19\n" + + "\bmedia_id\x18\x01 \x01(\tR\amediaId\x12\x1f\n" + + "\vanalysis_id\x18\x02 \x01(\tR\n" + + "analysisId\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12'\n" + + "\x0fprevious_status\x18\x04 \x01(\tR\x0epreviousStatus\x12#\n" + + "\rsource_plugin\x18\x05 \x01(\tR\fsourcePlugin\x12\x1f\n" + + "\vsource_type\x18\x06 \x01(\tR\n" + + "sourceType\x12\"\n" + + "\rsource_ref_id\x18\a \x01(\tR\vsourceRefId\x12!\n" + + "\fmoderated_by\x18\b \x01(\tR\vmoderatedBy\x12\x12\n" + + "\x04note\x18\t \x01(\tR\x04note*\xea\x02\n" + + "\x04Hook\x12\x14\n" + + "\x10HOOK_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11HOOK_RENDER_BLOCK\x10\x01\x12\x18\n" + + "\x14HOOK_RENDER_TEMPLATE\x10\x02\x12\x14\n" + + "\x10HOOK_HANDLE_HTTP\x10\x03\x12\f\n" + + "\bHOOK_JOB\x10\x04\x12\r\n" + + "\tHOOK_LOAD\x10\x05\x12\x0f\n" + + "\vHOOK_UNLOAD\x10\x06\x12\x12\n" + + "\x0eHOOK_RAG_FETCH\x10\a\x12\x13\n" + + "\x0fHOOK_MEDIA_HOOK\x10\b\x12\x11\n" + + "\rHOOK_DESCRIBE\x10\t\x12\x13\n" + + "\x0fHOOK_RENDER_TAG\x10\n" + + "\x12\x15\n" + + "\x11HOOK_APPLY_FILTER\x10\v\x12\x15\n" + + "\x11HOOK_AI_TOOL_CALL\x10\f\x12\x14\n" + + "\x10HOOK_BRIDGE_CALL\x10\r\x12 \n" + + "\x1cHOOK_DIRECTORY_PANEL_SECTION\x10\x0e\x12 \n" + + "\x1cHOOK_DIRECTORY_PIN_DECORATOR\x10\x0f*\xf3\x01\n" + + "\fAbiErrorCode\x12\x1e\n" + + "\x1aABI_ERROR_CODE_UNSPECIFIED\x10\x00\x12\x1b\n" + + "\x17ABI_ERROR_CODE_INTERNAL\x10\x01\x12\x19\n" + + "\x15ABI_ERROR_CODE_DECODE\x10\x02\x12 \n" + + "\x1cABI_ERROR_CODE_UNIMPLEMENTED\x10\x03\x12$\n" + + " ABI_ERROR_CODE_DEADLINE_EXCEEDED\x10\x04\x12$\n" + + " ABI_ERROR_CODE_PERMISSION_DENIED\x10\x05\x12\x1d\n" + + "\x19ABI_ERROR_CODE_TX_EXPIRED\x10\x06B5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3" + +var ( + file_v1_invoke_proto_rawDescOnce sync.Once + file_v1_invoke_proto_rawDescData []byte +) + +func file_v1_invoke_proto_rawDescGZIP() []byte { + file_v1_invoke_proto_rawDescOnce.Do(func() { + file_v1_invoke_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_invoke_proto_rawDesc), len(file_v1_invoke_proto_rawDesc))) + }) + return file_v1_invoke_proto_rawDescData +} + +var file_v1_invoke_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_v1_invoke_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_v1_invoke_proto_goTypes = []any{ + (Hook)(0), // 0: abi.v1.Hook + (AbiErrorCode)(0), // 1: abi.v1.AbiErrorCode + (*InvokeRequest)(nil), // 2: abi.v1.InvokeRequest + (*InvokeResponse)(nil), // 3: abi.v1.InvokeResponse + (*AbiError)(nil), // 4: abi.v1.AbiError + (*DescribeRequest)(nil), // 5: abi.v1.DescribeRequest + (*DescribeResponse)(nil), // 6: abi.v1.DescribeResponse + (*LoadRequest)(nil), // 7: abi.v1.LoadRequest + (*HostConfig)(nil), // 8: abi.v1.HostConfig + (*LoadResponse)(nil), // 9: abi.v1.LoadResponse + (*UnloadRequest)(nil), // 10: abi.v1.UnloadRequest + (*UnloadResponse)(nil), // 11: abi.v1.UnloadResponse + (*JobRequest)(nil), // 12: abi.v1.JobRequest + (*JobResponse)(nil), // 13: abi.v1.JobResponse + (*RagFetchRequest)(nil), // 14: abi.v1.RagFetchRequest + (*RagFetchResponse)(nil), // 15: abi.v1.RagFetchResponse + (*MediaHookRequest)(nil), // 16: abi.v1.MediaHookRequest + (*MediaHookResponse)(nil), // 17: abi.v1.MediaHookResponse + (*MediaAnalyzedEvent)(nil), // 18: abi.v1.MediaAnalyzedEvent + (*AiToolCallRequest)(nil), // 19: abi.v1.AiToolCallRequest + (*AiToolCallResponse)(nil), // 20: abi.v1.AiToolCallResponse + (*BridgeCallRequest)(nil), // 21: abi.v1.BridgeCallRequest + (*BridgeCallResponse)(nil), // 22: abi.v1.BridgeCallResponse + (*DirectoryPanelSectionRequest)(nil), // 23: abi.v1.DirectoryPanelSectionRequest + (*DirectoryPanelSectionResponse)(nil), // 24: abi.v1.DirectoryPanelSectionResponse + (*DirectoryPinDecoratorRequest)(nil), // 25: abi.v1.DirectoryPinDecoratorRequest + (*DirectoryPinDecoratorResponse)(nil), // 26: abi.v1.DirectoryPinDecoratorResponse + (*ModerationDecisionEvent)(nil), // 27: abi.v1.ModerationDecisionEvent + (*PluginManifest)(nil), // 28: abi.v1.PluginManifest +} +var file_v1_invoke_proto_depIdxs = []int32{ + 0, // 0: abi.v1.InvokeRequest.hook:type_name -> abi.v1.Hook + 4, // 1: abi.v1.InvokeResponse.error:type_name -> abi.v1.AbiError + 1, // 2: abi.v1.AbiError.code:type_name -> abi.v1.AbiErrorCode + 28, // 3: abi.v1.DescribeResponse.manifest:type_name -> abi.v1.PluginManifest + 8, // 4: abi.v1.LoadRequest.host_config:type_name -> abi.v1.HostConfig + 18, // 5: abi.v1.MediaHookRequest.media_analyzed:type_name -> abi.v1.MediaAnalyzedEvent + 27, // 6: abi.v1.MediaHookRequest.moderation_decision:type_name -> abi.v1.ModerationDecisionEvent + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_v1_invoke_proto_init() } +func file_v1_invoke_proto_init() { + if File_v1_invoke_proto != nil { + return + } + file_v1_manifest_proto_init() + file_v1_invoke_proto_msgTypes[14].OneofWrappers = []any{ + (*MediaHookRequest_MediaAnalyzed)(nil), + (*MediaHookRequest_ModerationDecision)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_invoke_proto_rawDesc), len(file_v1_invoke_proto_rawDesc)), + NumEnums: 2, + NumMessages: 26, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_invoke_proto_goTypes, + DependencyIndexes: file_v1_invoke_proto_depIdxs, + EnumInfos: file_v1_invoke_proto_enumTypes, + MessageInfos: file_v1_invoke_proto_msgTypes, + }.Build() + File_v1_invoke_proto = out.File + file_v1_invoke_proto_goTypes = nil + file_v1_invoke_proto_depIdxs = nil +} diff --git a/abi/v1/manifest.pb.go b/abi/v1/manifest.pb.go new file mode 100644 index 0000000..9549e7f --- /dev/null +++ b/abi/v1/manifest.pb.go @@ -0,0 +1,1507 @@ +// manifest.proto — the static plugin manifest (WO-WZ-001). +// +// PluginManifest is the wire form of everything that is *static data* in +// core/plugin/registration.go (PluginRegistration). It is produced once at +// publish time by calling the guest's DESCRIBE hook and stored as manifest.pb +// inside the .bnp artifact; the CMS loader reads it without instantiating the +// wasm module. +// +// Function-valued registration fields cannot cross the wire; they map to +// either a hook (Load/Unload/JobHandlers/MediaHooks/HTTPHandler), a boolean +// presence flag here, or an artifact directory (Assets → assets/, +// Schemas → schemas/, Migrations → migrations/). See core/docs/wasm-abi.md +// for the full field-by-field mapping. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: v1/manifest.proto + +package abiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// PluginManifest mirrors the static surface of plugin.PluginRegistration. +type PluginManifest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ABI major version the plugin was built against. The host rejects + // manifests whose major version it does not support. + AbiVersion uint32 `protobuf:"varint,1,opt,name=abi_version,json=abiVersion,proto3" json:"abi_version,omitempty"` + // PluginRegistration.Name / .Version. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + // PluginRegistration.Dependencies. + Dependencies []*Dependency `protobuf:"bytes,4,rep,name=dependencies,proto3" json:"dependencies,omitempty"` + // Blocks registered via BlockRegistry.Register (captured by DESCRIBE). + Blocks []*BlockMeta `protobuf:"bytes,5,rep,name=blocks,proto3" json:"blocks,omitempty"` + // Blocks registered via BlockRegistry.RegisterTemplateOverride[WithSource]. + BlockTemplateOverrides []*BlockTemplateOverride `protobuf:"bytes,6,rep,name=block_template_overrides,json=blockTemplateOverrides,proto3" json:"block_template_overrides,omitempty"` + // Templates registered via TemplateRegistry (captured by DESCRIBE). + TemplateKeys []string `protobuf:"bytes,7,rep,name=template_keys,json=templateKeys,proto3" json:"template_keys,omitempty"` // TemplateRegistry.Register + SystemTemplates []*SystemTemplateMeta `protobuf:"bytes,8,rep,name=system_templates,json=systemTemplates,proto3" json:"system_templates,omitempty"` // RegisterSystemTemplate + PageTemplates []*PageTemplateMeta `protobuf:"bytes,9,rep,name=page_templates,json=pageTemplates,proto3" json:"page_templates,omitempty"` // RegisterPageTemplate + EmailWrapperSystemKeys []string `protobuf:"bytes,10,rep,name=email_wrapper_system_keys,json=emailWrapperSystemKeys,proto3" json:"email_wrapper_system_keys,omitempty"` // RegisterEmailWrapper + // PluginRegistration.AdminPages. + AdminPages []*AdminPage `protobuf:"bytes,11,rep,name=admin_pages,json=adminPages,proto3" json:"admin_pages,omitempty"` + // PluginRegistration.SettingsSchema / .ThemePresets / .BundledFonts (JSON). + SettingsSchema []byte `protobuf:"bytes,12,opt,name=settings_schema,json=settingsSchema,proto3" json:"settings_schema,omitempty"` + ThemePresets []byte `protobuf:"bytes,13,opt,name=theme_presets,json=themePresets,proto3" json:"theme_presets,omitempty"` + BundledFonts []byte `protobuf:"bytes,14,opt,name=bundled_fonts,json=bundledFonts,proto3" json:"bundled_fonts,omitempty"` + // PluginRegistration.MasterPages. + MasterPages []*MasterPageDefinition `protobuf:"bytes,15,rep,name=master_pages,json=masterPages,proto3" json:"master_pages,omitempty"` + // PluginRegistration.AIActions. + AiActions []*AiAction `protobuf:"bytes,16,rep,name=ai_actions,json=aiActions,proto3" json:"ai_actions,omitempty"` + // RBAC roles for the plugin's own Connect services, merged from + // ServiceRegistration (full method name → role, e.g. + // "/symposium.v1.ForumService/CreateThread" → "admin"). + RbacMethodRoles map[string]string `protobuf:"bytes,17,rep,name=rbac_method_roles,json=rbacMethodRoles,proto3" json:"rbac_method_roles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // PluginRegistration.CSSManifest. + CssManifest *CssManifest `protobuf:"bytes,18,opt,name=css_manifest,json=cssManifest,proto3" json:"css_manifest,omitempty"` + // PluginRegistration.RequiredIconPacks. + RequiredIconPacks []string `protobuf:"bytes,19,rep,name=required_icon_packs,json=requiredIconPacks,proto3" json:"required_icon_packs,omitempty"` + // PluginRegistration.DirectoryExtensions (static fields only; the + // panel-section / pin-decorator callbacks are counted so the host knows + // how many guest callbacks exist). + DirectoryExtensions *DirectoryExtensions `protobuf:"bytes,20,opt,name=directory_extensions,json=directoryExtensions,proto3" json:"directory_extensions,omitempty"` + // Job types the plugin handles (keys of PluginRegistration.JobHandlers). + // The host dispatches these via the JOB hook. + JobTypes []string `protobuf:"bytes,21,rep,name=job_types,json=jobTypes,proto3" json:"job_types,omitempty"` + // Content types the plugin registered RAG content fetchers for + // (RAGService.RegisterContentFetcher inverts to a manifest declaration; + // the host calls back via the RAG_FETCH hook). + RagContentFetcherTypes []string `protobuf:"bytes,22,rep,name=rag_content_fetcher_types,json=ragContentFetcherTypes,proto3" json:"rag_content_fetcher_types,omitempty"` + // PluginRegistration.SettingsPanel — Module Federation path of the + // settings panel component ("" when the plugin has none). + SettingsPanel string `protobuf:"bytes,23,opt,name=settings_panel,json=settingsPanel,proto3" json:"settings_panel,omitempty"` + // Presence flags for function-valued registration fields that invert to + // hooks at runtime. + HasHttpHandler bool `protobuf:"varint,24,opt,name=has_http_handler,json=hasHttpHandler,proto3" json:"has_http_handler,omitempty"` // PluginRegistration.HTTPHandler → HANDLE_HTTP + HasLoadHook bool `protobuf:"varint,25,opt,name=has_load_hook,json=hasLoadHook,proto3" json:"has_load_hook,omitempty"` // PluginRegistration.Load → LOAD + HasUnloadHook bool `protobuf:"varint,26,opt,name=has_unload_hook,json=hasUnloadHook,proto3" json:"has_unload_hook,omitempty"` // PluginRegistration.Unload → UNLOAD + HasMediaHooks bool `protobuf:"varint,27,opt,name=has_media_hooks,json=hasMediaHooks,proto3" json:"has_media_hooks,omitempty"` // PluginRegistration.MediaHooks → MEDIA_HOOK + HasProvisioner bool `protobuf:"varint,28,opt,name=has_provisioner,json=hasProvisioner,proto3" json:"has_provisioner,omitempty"` // PluginRegistration.RegisterWithProvisioner + // Core CMS services the plugin mounts with custom RBAC roles + // (CoreServiceBindings.Bind becomes a static declaration; the host + // constructs and mounts the handlers). + CoreServiceBindings []*CoreServiceBinding `protobuf:"bytes,29,rep,name=core_service_bindings,json=coreServiceBindings,proto3" json:"core_service_bindings,omitempty"` + // data_dir requests a persistent per-plugin /data preopen at load. Its + // source of truth is the plugin.mod `data_dir = true` key, which lives + // outside the guest code, so DESCRIBE cannot populate it: the packer + // (`ninja plugin build`) stamps it into the manifest from plugin.mod so + // the loader reads one source. OFF by default. + DataDir bool `protobuf:"varint,30,opt,name=data_dir,json=dataDir,proto3" json:"data_dir,omitempty"` + // Template tags the plugin provides (blocks.RegisterTag). For each name the + // host registers a pongo2 tag that invokes the guest via HOOK_RENDER_TAG. + // Captured by DESCRIBE from the blocks tag registry (Register-time). + DeclaredTags []string `protobuf:"bytes,31,rep,name=declared_tags,json=declaredTags,proto3" json:"declared_tags,omitempty"` + // Template filters the plugin provides (blocks.RegisterFilter). For each + // name the host registers a pongo2 filter that invokes the guest via + // HOOK_APPLY_FILTER. Captured by DESCRIBE from the blocks filter registry. + DeclaredFilters []string `protobuf:"bytes,32,rep,name=declared_filters,json=declaredFilters,proto3" json:"declared_filters,omitempty"` + // codeless marks a declarative artifact with NO plugin.wasm (WO-WZ-020): + // the host runs it entirely — block definitions from the artifact's + // blocks/ dir (blocks.yaml manifest-FS layout), seed data from seed/, + // assets/migrations as usual — and never instantiates a guest. A codeless + // manifest MUST NOT declare any computing hook (http handler, jobs, + // load/unload, RAG fetchers, media hooks, tags/filters, provisioner, + // service bindings, directory callbacks); the packer and the host reader + // both reject the combination. Not produced by DESCRIBE (there is no guest + // to describe): `ninja plugin build` synthesizes the manifest from + // plugin.mod + the artifact's declarative files. + Codeless bool `protobuf:"varint,33,opt,name=codeless,proto3" json:"codeless,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PluginManifest) Reset() { + *x = PluginManifest{} + mi := &file_v1_manifest_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PluginManifest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PluginManifest) ProtoMessage() {} + +func (x *PluginManifest) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PluginManifest.ProtoReflect.Descriptor instead. +func (*PluginManifest) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{0} +} + +func (x *PluginManifest) GetAbiVersion() uint32 { + if x != nil { + return x.AbiVersion + } + return 0 +} + +func (x *PluginManifest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PluginManifest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *PluginManifest) GetDependencies() []*Dependency { + if x != nil { + return x.Dependencies + } + return nil +} + +func (x *PluginManifest) GetBlocks() []*BlockMeta { + if x != nil { + return x.Blocks + } + return nil +} + +func (x *PluginManifest) GetBlockTemplateOverrides() []*BlockTemplateOverride { + if x != nil { + return x.BlockTemplateOverrides + } + return nil +} + +func (x *PluginManifest) GetTemplateKeys() []string { + if x != nil { + return x.TemplateKeys + } + return nil +} + +func (x *PluginManifest) GetSystemTemplates() []*SystemTemplateMeta { + if x != nil { + return x.SystemTemplates + } + return nil +} + +func (x *PluginManifest) GetPageTemplates() []*PageTemplateMeta { + if x != nil { + return x.PageTemplates + } + return nil +} + +func (x *PluginManifest) GetEmailWrapperSystemKeys() []string { + if x != nil { + return x.EmailWrapperSystemKeys + } + return nil +} + +func (x *PluginManifest) GetAdminPages() []*AdminPage { + if x != nil { + return x.AdminPages + } + return nil +} + +func (x *PluginManifest) GetSettingsSchema() []byte { + if x != nil { + return x.SettingsSchema + } + return nil +} + +func (x *PluginManifest) GetThemePresets() []byte { + if x != nil { + return x.ThemePresets + } + return nil +} + +func (x *PluginManifest) GetBundledFonts() []byte { + if x != nil { + return x.BundledFonts + } + return nil +} + +func (x *PluginManifest) GetMasterPages() []*MasterPageDefinition { + if x != nil { + return x.MasterPages + } + return nil +} + +func (x *PluginManifest) GetAiActions() []*AiAction { + if x != nil { + return x.AiActions + } + return nil +} + +func (x *PluginManifest) GetRbacMethodRoles() map[string]string { + if x != nil { + return x.RbacMethodRoles + } + return nil +} + +func (x *PluginManifest) GetCssManifest() *CssManifest { + if x != nil { + return x.CssManifest + } + return nil +} + +func (x *PluginManifest) GetRequiredIconPacks() []string { + if x != nil { + return x.RequiredIconPacks + } + return nil +} + +func (x *PluginManifest) GetDirectoryExtensions() *DirectoryExtensions { + if x != nil { + return x.DirectoryExtensions + } + return nil +} + +func (x *PluginManifest) GetJobTypes() []string { + if x != nil { + return x.JobTypes + } + return nil +} + +func (x *PluginManifest) GetRagContentFetcherTypes() []string { + if x != nil { + return x.RagContentFetcherTypes + } + return nil +} + +func (x *PluginManifest) GetSettingsPanel() string { + if x != nil { + return x.SettingsPanel + } + return "" +} + +func (x *PluginManifest) GetHasHttpHandler() bool { + if x != nil { + return x.HasHttpHandler + } + return false +} + +func (x *PluginManifest) GetHasLoadHook() bool { + if x != nil { + return x.HasLoadHook + } + return false +} + +func (x *PluginManifest) GetHasUnloadHook() bool { + if x != nil { + return x.HasUnloadHook + } + return false +} + +func (x *PluginManifest) GetHasMediaHooks() bool { + if x != nil { + return x.HasMediaHooks + } + return false +} + +func (x *PluginManifest) GetHasProvisioner() bool { + if x != nil { + return x.HasProvisioner + } + return false +} + +func (x *PluginManifest) GetCoreServiceBindings() []*CoreServiceBinding { + if x != nil { + return x.CoreServiceBindings + } + return nil +} + +func (x *PluginManifest) GetDataDir() bool { + if x != nil { + return x.DataDir + } + return false +} + +func (x *PluginManifest) GetDeclaredTags() []string { + if x != nil { + return x.DeclaredTags + } + return nil +} + +func (x *PluginManifest) GetDeclaredFilters() []string { + if x != nil { + return x.DeclaredFilters + } + return nil +} + +func (x *PluginManifest) GetCodeless() bool { + if x != nil { + return x.Codeless + } + return false +} + +// Dependency mirrors plugin.Dependency. +type Dependency struct { + state protoimpl.MessageState `protogen:"open.v1"` + Plugin string `protobuf:"bytes,1,opt,name=plugin,proto3" json:"plugin,omitempty"` + MinVersion string `protobuf:"bytes,2,opt,name=min_version,json=minVersion,proto3" json:"min_version,omitempty"` + Required bool `protobuf:"varint,3,opt,name=required,proto3" json:"required,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Dependency) Reset() { + *x = Dependency{} + mi := &file_v1_manifest_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Dependency) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Dependency) ProtoMessage() {} + +func (x *Dependency) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Dependency.ProtoReflect.Descriptor instead. +func (*Dependency) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{1} +} + +func (x *Dependency) GetPlugin() string { + if x != nil { + return x.Plugin + } + return "" +} + +func (x *Dependency) GetMinVersion() string { + if x != nil { + return x.MinVersion + } + return "" +} + +func (x *Dependency) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +// BlockMeta mirrors blocks.BlockMeta. Source is omitted: the host assigns it +// from the manifest's plugin name at load time. +type BlockMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // blocks.BlockCategory string ("content", "layout", "navigation", "blog", + // "theme"). + Category string `protobuf:"bytes,4,opt,name=category,proto3" json:"category,omitempty"` + HasInternalSlot bool `protobuf:"varint,5,opt,name=has_internal_slot,json=hasInternalSlot,proto3" json:"has_internal_slot,omitempty"` + Hidden bool `protobuf:"varint,6,opt,name=hidden,proto3" json:"hidden,omitempty"` + EditorJs string `protobuf:"bytes,7,opt,name=editor_js,json=editorJs,proto3" json:"editor_js,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BlockMeta) Reset() { + *x = BlockMeta{} + mi := &file_v1_manifest_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlockMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlockMeta) ProtoMessage() {} + +func (x *BlockMeta) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlockMeta.ProtoReflect.Descriptor instead. +func (*BlockMeta) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{2} +} + +func (x *BlockMeta) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *BlockMeta) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *BlockMeta) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *BlockMeta) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *BlockMeta) GetHasInternalSlot() bool { + if x != nil { + return x.HasInternalSlot + } + return false +} + +func (x *BlockMeta) GetHidden() bool { + if x != nil { + return x.Hidden + } + return false +} + +func (x *BlockMeta) GetEditorJs() string { + if x != nil { + return x.EditorJs + } + return "" +} + +// BlockTemplateOverride captures BlockRegistry.RegisterTemplateOverride and +// RegisterTemplateOverrideWithSource registrations. +type BlockTemplateOverride struct { + state protoimpl.MessageState `protogen:"open.v1"` + TemplateKey string `protobuf:"bytes,1,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` + BlockKey string `protobuf:"bytes,2,opt,name=block_key,json=blockKey,proto3" json:"block_key,omitempty"` + // Override source label; empty for plain RegisterTemplateOverride. + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BlockTemplateOverride) Reset() { + *x = BlockTemplateOverride{} + mi := &file_v1_manifest_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlockTemplateOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlockTemplateOverride) ProtoMessage() {} + +func (x *BlockTemplateOverride) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlockTemplateOverride.ProtoReflect.Descriptor instead. +func (*BlockTemplateOverride) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{3} +} + +func (x *BlockTemplateOverride) GetTemplateKey() string { + if x != nil { + return x.TemplateKey + } + return "" +} + +func (x *BlockTemplateOverride) GetBlockKey() string { + if x != nil { + return x.BlockKey + } + return "" +} + +func (x *BlockTemplateOverride) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +// SystemTemplateMeta mirrors templates.SystemTemplateMeta. +type SystemTemplateMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SystemTemplateMeta) Reset() { + *x = SystemTemplateMeta{} + mi := &file_v1_manifest_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SystemTemplateMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemTemplateMeta) ProtoMessage() {} + +func (x *SystemTemplateMeta) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SystemTemplateMeta.ProtoReflect.Descriptor instead. +func (*SystemTemplateMeta) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{4} +} + +func (x *SystemTemplateMeta) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *SystemTemplateMeta) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *SystemTemplateMeta) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// PageTemplateMeta mirrors templates.PageTemplateMeta plus the system +// template it was registered under. +type PageTemplateMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + SystemKey string `protobuf:"bytes,1,opt,name=system_key,json=systemKey,proto3" json:"system_key,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + Slots []string `protobuf:"bytes,5,rep,name=slots,proto3" json:"slots,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PageTemplateMeta) Reset() { + *x = PageTemplateMeta{} + mi := &file_v1_manifest_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PageTemplateMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PageTemplateMeta) ProtoMessage() {} + +func (x *PageTemplateMeta) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PageTemplateMeta.ProtoReflect.Descriptor instead. +func (*PageTemplateMeta) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{5} +} + +func (x *PageTemplateMeta) GetSystemKey() string { + if x != nil { + return x.SystemKey + } + return "" +} + +func (x *PageTemplateMeta) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *PageTemplateMeta) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *PageTemplateMeta) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PageTemplateMeta) GetSlots() []string { + if x != nil { + return x.Slots + } + return nil +} + +// AdminPage mirrors plugin.AdminPage. +type AdminPage struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Icon string `protobuf:"bytes,3,opt,name=icon,proto3" json:"icon,omitempty"` + Route string `protobuf:"bytes,4,opt,name=route,proto3" json:"route,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminPage) Reset() { + *x = AdminPage{} + mi := &file_v1_manifest_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminPage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminPage) ProtoMessage() {} + +func (x *AdminPage) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminPage.ProtoReflect.Descriptor instead. +func (*AdminPage) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{6} +} + +func (x *AdminPage) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AdminPage) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *AdminPage) GetIcon() string { + if x != nil { + return x.Icon + } + return "" +} + +func (x *AdminPage) GetRoute() string { + if x != nil { + return x.Route + } + return "" +} + +// AiAction mirrors plugin.AIAction. +type AiAction struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + DefaultProvider string `protobuf:"bytes,4,opt,name=default_provider,json=defaultProvider,proto3" json:"default_provider,omitempty"` + DefaultModel string `protobuf:"bytes,5,opt,name=default_model,json=defaultModel,proto3" json:"default_model,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AiAction) Reset() { + *x = AiAction{} + mi := &file_v1_manifest_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AiAction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AiAction) ProtoMessage() {} + +func (x *AiAction) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AiAction.ProtoReflect.Descriptor instead. +func (*AiAction) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{7} +} + +func (x *AiAction) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AiAction) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *AiAction) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AiAction) GetDefaultProvider() string { + if x != nil { + return x.DefaultProvider + } + return "" +} + +func (x *AiAction) GetDefaultModel() string { + if x != nil { + return x.DefaultModel + } + return "" +} + +// CssManifest mirrors plugin.CSSManifest. +type CssManifest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NpmPackages map[string]string `protobuf:"bytes,1,rep,name=npm_packages,json=npmPackages,proto3" json:"npm_packages,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + CssDirectives []string `protobuf:"bytes,2,rep,name=css_directives,json=cssDirectives,proto3" json:"css_directives,omitempty"` + InputCssAppend string `protobuf:"bytes,3,opt,name=input_css_append,json=inputCssAppend,proto3" json:"input_css_append,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CssManifest) Reset() { + *x = CssManifest{} + mi := &file_v1_manifest_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CssManifest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CssManifest) ProtoMessage() {} + +func (x *CssManifest) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CssManifest.ProtoReflect.Descriptor instead. +func (*CssManifest) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{8} +} + +func (x *CssManifest) GetNpmPackages() map[string]string { + if x != nil { + return x.NpmPackages + } + return nil +} + +func (x *CssManifest) GetCssDirectives() []string { + if x != nil { + return x.CssDirectives + } + return nil +} + +func (x *CssManifest) GetInputCssAppend() string { + if x != nil { + return x.InputCssAppend + } + return "" +} + +// DirectoryExtensions mirrors the static fields of plugin.DirectoryExtensions. +type DirectoryExtensions struct { + state protoimpl.MessageState `protogen:"open.v1"` + BooleanFilterFields []string `protobuf:"bytes,1,rep,name=boolean_filter_fields,json=booleanFilterFields,proto3" json:"boolean_filter_fields,omitempty"` + SelectFilterFields []string `protobuf:"bytes,2,rep,name=select_filter_fields,json=selectFilterFields,proto3" json:"select_filter_fields,omitempty"` + BadgeLabels map[string]*BadgeLabel `protobuf:"bytes,3,rep,name=badge_labels,json=badgeLabels,proto3" json:"badge_labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Counts of the callback slices (PanelSections / PinDecorators) so the host + // knows how many guest callbacks the plugin registered. Their invocation + // hook is a runtime-WO concern. + PanelSectionCount uint32 `protobuf:"varint,4,opt,name=panel_section_count,json=panelSectionCount,proto3" json:"panel_section_count,omitempty"` + PinDecoratorCount uint32 `protobuf:"varint,5,opt,name=pin_decorator_count,json=pinDecoratorCount,proto3" json:"pin_decorator_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectoryExtensions) Reset() { + *x = DirectoryExtensions{} + mi := &file_v1_manifest_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectoryExtensions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectoryExtensions) ProtoMessage() {} + +func (x *DirectoryExtensions) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectoryExtensions.ProtoReflect.Descriptor instead. +func (*DirectoryExtensions) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{9} +} + +func (x *DirectoryExtensions) GetBooleanFilterFields() []string { + if x != nil { + return x.BooleanFilterFields + } + return nil +} + +func (x *DirectoryExtensions) GetSelectFilterFields() []string { + if x != nil { + return x.SelectFilterFields + } + return nil +} + +func (x *DirectoryExtensions) GetBadgeLabels() map[string]*BadgeLabel { + if x != nil { + return x.BadgeLabels + } + return nil +} + +func (x *DirectoryExtensions) GetPanelSectionCount() uint32 { + if x != nil { + return x.PanelSectionCount + } + return 0 +} + +func (x *DirectoryExtensions) GetPinDecoratorCount() uint32 { + if x != nil { + return x.PinDecoratorCount + } + return 0 +} + +// BadgeLabel mirrors the [2]string value of DirectoryExtensions.BadgeLabels. +type BadgeLabel struct { + state protoimpl.MessageState `protogen:"open.v1"` + Positive string `protobuf:"bytes,1,opt,name=positive,proto3" json:"positive,omitempty"` + Negative string `protobuf:"bytes,2,opt,name=negative,proto3" json:"negative,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BadgeLabel) Reset() { + *x = BadgeLabel{} + mi := &file_v1_manifest_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BadgeLabel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BadgeLabel) ProtoMessage() {} + +func (x *BadgeLabel) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BadgeLabel.ProtoReflect.Descriptor instead. +func (*BadgeLabel) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{10} +} + +func (x *BadgeLabel) GetPositive() string { + if x != nil { + return x.Positive + } + return "" +} + +func (x *BadgeLabel) GetNegative() string { + if x != nil { + return x.Negative + } + return "" +} + +// MasterPageDefinition mirrors plugin.MasterPageDefinition. +type MasterPageDefinition struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + PageTemplates []string `protobuf:"bytes,3,rep,name=page_templates,json=pageTemplates,proto3" json:"page_templates,omitempty"` + Blocks []*MasterPageBlock `protobuf:"bytes,4,rep,name=blocks,proto3" json:"blocks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MasterPageDefinition) Reset() { + *x = MasterPageDefinition{} + mi := &file_v1_manifest_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MasterPageDefinition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MasterPageDefinition) ProtoMessage() {} + +func (x *MasterPageDefinition) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MasterPageDefinition.ProtoReflect.Descriptor instead. +func (*MasterPageDefinition) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{11} +} + +func (x *MasterPageDefinition) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *MasterPageDefinition) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *MasterPageDefinition) GetPageTemplates() []string { + if x != nil { + return x.PageTemplates + } + return nil +} + +func (x *MasterPageDefinition) GetBlocks() []*MasterPageBlock { + if x != nil { + return x.Blocks + } + return nil +} + +// MasterPageBlock mirrors plugin.MasterPageBlock. +type MasterPageBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + BlockKey string `protobuf:"bytes,1,opt,name=block_key,json=blockKey,proto3" json:"block_key,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // JSON encoding of the block's content map. + ContentJson []byte `protobuf:"bytes,3,opt,name=content_json,json=contentJson,proto3" json:"content_json,omitempty"` + HtmlContent *string `protobuf:"bytes,4,opt,name=html_content,json=htmlContent,proto3,oneof" json:"html_content,omitempty"` + Slot string `protobuf:"bytes,5,opt,name=slot,proto3" json:"slot,omitempty"` + SortOrder int32 `protobuf:"varint,6,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MasterPageBlock) Reset() { + *x = MasterPageBlock{} + mi := &file_v1_manifest_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MasterPageBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MasterPageBlock) ProtoMessage() {} + +func (x *MasterPageBlock) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MasterPageBlock.ProtoReflect.Descriptor instead. +func (*MasterPageBlock) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{12} +} + +func (x *MasterPageBlock) GetBlockKey() string { + if x != nil { + return x.BlockKey + } + return "" +} + +func (x *MasterPageBlock) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *MasterPageBlock) GetContentJson() []byte { + if x != nil { + return x.ContentJson + } + return nil +} + +func (x *MasterPageBlock) GetHtmlContent() string { + if x != nil && x.HtmlContent != nil { + return *x.HtmlContent + } + return "" +} + +func (x *MasterPageBlock) GetSlot() string { + if x != nil { + return x.Slot + } + return "" +} + +func (x *MasterPageBlock) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +// CoreServiceBinding mirrors a plugin.CoreServiceBindings.Bind call: mount +// the named core-provided Connect service with these RBAC roles. +type CoreServiceBinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + MethodRoles map[string]string `protobuf:"bytes,2,rep,name=method_roles,json=methodRoles,proto3" json:"method_roles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CoreServiceBinding) Reset() { + *x = CoreServiceBinding{} + mi := &file_v1_manifest_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CoreServiceBinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CoreServiceBinding) ProtoMessage() {} + +func (x *CoreServiceBinding) ProtoReflect() protoreflect.Message { + mi := &file_v1_manifest_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CoreServiceBinding.ProtoReflect.Descriptor instead. +func (*CoreServiceBinding) Descriptor() ([]byte, []int) { + return file_v1_manifest_proto_rawDescGZIP(), []int{13} +} + +func (x *CoreServiceBinding) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *CoreServiceBinding) GetMethodRoles() map[string]string { + if x != nil { + return x.MethodRoles + } + return nil +} + +var File_v1_manifest_proto protoreflect.FileDescriptor + +const file_v1_manifest_proto_rawDesc = "" + + "\n" + + "\x11v1/manifest.proto\x12\x06abi.v1\"\x8e\r\n" + + "\x0ePluginManifest\x12\x1f\n" + + "\vabi_version\x18\x01 \x01(\rR\n" + + "abiVersion\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\aversion\x18\x03 \x01(\tR\aversion\x126\n" + + "\fdependencies\x18\x04 \x03(\v2\x12.abi.v1.DependencyR\fdependencies\x12)\n" + + "\x06blocks\x18\x05 \x03(\v2\x11.abi.v1.BlockMetaR\x06blocks\x12W\n" + + "\x18block_template_overrides\x18\x06 \x03(\v2\x1d.abi.v1.BlockTemplateOverrideR\x16blockTemplateOverrides\x12#\n" + + "\rtemplate_keys\x18\a \x03(\tR\ftemplateKeys\x12E\n" + + "\x10system_templates\x18\b \x03(\v2\x1a.abi.v1.SystemTemplateMetaR\x0fsystemTemplates\x12?\n" + + "\x0epage_templates\x18\t \x03(\v2\x18.abi.v1.PageTemplateMetaR\rpageTemplates\x129\n" + + "\x19email_wrapper_system_keys\x18\n" + + " \x03(\tR\x16emailWrapperSystemKeys\x122\n" + + "\vadmin_pages\x18\v \x03(\v2\x11.abi.v1.AdminPageR\n" + + "adminPages\x12'\n" + + "\x0fsettings_schema\x18\f \x01(\fR\x0esettingsSchema\x12#\n" + + "\rtheme_presets\x18\r \x01(\fR\fthemePresets\x12#\n" + + "\rbundled_fonts\x18\x0e \x01(\fR\fbundledFonts\x12?\n" + + "\fmaster_pages\x18\x0f \x03(\v2\x1c.abi.v1.MasterPageDefinitionR\vmasterPages\x12/\n" + + "\n" + + "ai_actions\x18\x10 \x03(\v2\x10.abi.v1.AiActionR\taiActions\x12W\n" + + "\x11rbac_method_roles\x18\x11 \x03(\v2+.abi.v1.PluginManifest.RbacMethodRolesEntryR\x0frbacMethodRoles\x126\n" + + "\fcss_manifest\x18\x12 \x01(\v2\x13.abi.v1.CssManifestR\vcssManifest\x12.\n" + + "\x13required_icon_packs\x18\x13 \x03(\tR\x11requiredIconPacks\x12N\n" + + "\x14directory_extensions\x18\x14 \x01(\v2\x1b.abi.v1.DirectoryExtensionsR\x13directoryExtensions\x12\x1b\n" + + "\tjob_types\x18\x15 \x03(\tR\bjobTypes\x129\n" + + "\x19rag_content_fetcher_types\x18\x16 \x03(\tR\x16ragContentFetcherTypes\x12%\n" + + "\x0esettings_panel\x18\x17 \x01(\tR\rsettingsPanel\x12(\n" + + "\x10has_http_handler\x18\x18 \x01(\bR\x0ehasHttpHandler\x12\"\n" + + "\rhas_load_hook\x18\x19 \x01(\bR\vhasLoadHook\x12&\n" + + "\x0fhas_unload_hook\x18\x1a \x01(\bR\rhasUnloadHook\x12&\n" + + "\x0fhas_media_hooks\x18\x1b \x01(\bR\rhasMediaHooks\x12'\n" + + "\x0fhas_provisioner\x18\x1c \x01(\bR\x0ehasProvisioner\x12N\n" + + "\x15core_service_bindings\x18\x1d \x03(\v2\x1a.abi.v1.CoreServiceBindingR\x13coreServiceBindings\x12\x19\n" + + "\bdata_dir\x18\x1e \x01(\bR\adataDir\x12#\n" + + "\rdeclared_tags\x18\x1f \x03(\tR\fdeclaredTags\x12)\n" + + "\x10declared_filters\x18 \x03(\tR\x0fdeclaredFilters\x12\x1a\n" + + "\bcodeless\x18! \x01(\bR\bcodeless\x1aB\n" + + "\x14RbacMethodRolesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"a\n" + + "\n" + + "Dependency\x12\x16\n" + + "\x06plugin\x18\x01 \x01(\tR\x06plugin\x12\x1f\n" + + "\vmin_version\x18\x02 \x01(\tR\n" + + "minVersion\x12\x1a\n" + + "\brequired\x18\x03 \x01(\bR\brequired\"\xd2\x01\n" + + "\tBlockMeta\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x1a\n" + + "\bcategory\x18\x04 \x01(\tR\bcategory\x12*\n" + + "\x11has_internal_slot\x18\x05 \x01(\bR\x0fhasInternalSlot\x12\x16\n" + + "\x06hidden\x18\x06 \x01(\bR\x06hidden\x12\x1b\n" + + "\teditor_js\x18\a \x01(\tR\beditorJs\"o\n" + + "\x15BlockTemplateOverride\x12!\n" + + "\ftemplate_key\x18\x01 \x01(\tR\vtemplateKey\x12\x1b\n" + + "\tblock_key\x18\x02 \x01(\tR\bblockKey\x12\x16\n" + + "\x06source\x18\x03 \x01(\tR\x06source\"^\n" + + "\x12SystemTemplateMeta\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\"\x91\x01\n" + + "\x10PageTemplateMeta\x12\x1d\n" + + "\n" + + "system_key\x18\x01 \x01(\tR\tsystemKey\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x04 \x01(\tR\vdescription\x12\x14\n" + + "\x05slots\x18\x05 \x03(\tR\x05slots\"]\n" + + "\tAdminPage\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x12\n" + + "\x04icon\x18\x03 \x01(\tR\x04icon\x12\x14\n" + + "\x05route\x18\x04 \x01(\tR\x05route\"\xa4\x01\n" + + "\bAiAction\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12)\n" + + "\x10default_provider\x18\x04 \x01(\tR\x0fdefaultProvider\x12#\n" + + "\rdefault_model\x18\x05 \x01(\tR\fdefaultModel\"\xe7\x01\n" + + "\vCssManifest\x12G\n" + + "\fnpm_packages\x18\x01 \x03(\v2$.abi.v1.CssManifest.NpmPackagesEntryR\vnpmPackages\x12%\n" + + "\x0ecss_directives\x18\x02 \x03(\tR\rcssDirectives\x12(\n" + + "\x10input_css_append\x18\x03 \x01(\tR\x0einputCssAppend\x1a>\n" + + "\x10NpmPackagesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x80\x03\n" + + "\x13DirectoryExtensions\x122\n" + + "\x15boolean_filter_fields\x18\x01 \x03(\tR\x13booleanFilterFields\x120\n" + + "\x14select_filter_fields\x18\x02 \x03(\tR\x12selectFilterFields\x12O\n" + + "\fbadge_labels\x18\x03 \x03(\v2,.abi.v1.DirectoryExtensions.BadgeLabelsEntryR\vbadgeLabels\x12.\n" + + "\x13panel_section_count\x18\x04 \x01(\rR\x11panelSectionCount\x12.\n" + + "\x13pin_decorator_count\x18\x05 \x01(\rR\x11pinDecoratorCount\x1aR\n" + + "\x10BadgeLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12(\n" + + "\x05value\x18\x02 \x01(\v2\x12.abi.v1.BadgeLabelR\x05value:\x028\x01\"D\n" + + "\n" + + "BadgeLabel\x12\x1a\n" + + "\bpositive\x18\x01 \x01(\tR\bpositive\x12\x1a\n" + + "\bnegative\x18\x02 \x01(\tR\bnegative\"\x96\x01\n" + + "\x14MasterPageDefinition\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12%\n" + + "\x0epage_templates\x18\x03 \x03(\tR\rpageTemplates\x12/\n" + + "\x06blocks\x18\x04 \x03(\v2\x17.abi.v1.MasterPageBlockR\x06blocks\"\xd3\x01\n" + + "\x0fMasterPageBlock\x12\x1b\n" + + "\tblock_key\x18\x01 \x01(\tR\bblockKey\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12!\n" + + "\fcontent_json\x18\x03 \x01(\fR\vcontentJson\x12&\n" + + "\fhtml_content\x18\x04 \x01(\tH\x00R\vhtmlContent\x88\x01\x01\x12\x12\n" + + "\x04slot\x18\x05 \x01(\tR\x04slot\x12\x1d\n" + + "\n" + + "sort_order\x18\x06 \x01(\x05R\tsortOrderB\x0f\n" + + "\r_html_content\"\xc7\x01\n" + + "\x12CoreServiceBinding\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12N\n" + + "\fmethod_roles\x18\x02 \x03(\v2+.abi.v1.CoreServiceBinding.MethodRolesEntryR\vmethodRoles\x1a>\n" + + "\x10MethodRolesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3" + +var ( + file_v1_manifest_proto_rawDescOnce sync.Once + file_v1_manifest_proto_rawDescData []byte +) + +func file_v1_manifest_proto_rawDescGZIP() []byte { + file_v1_manifest_proto_rawDescOnce.Do(func() { + file_v1_manifest_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_manifest_proto_rawDesc), len(file_v1_manifest_proto_rawDesc))) + }) + return file_v1_manifest_proto_rawDescData +} + +var file_v1_manifest_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_v1_manifest_proto_goTypes = []any{ + (*PluginManifest)(nil), // 0: abi.v1.PluginManifest + (*Dependency)(nil), // 1: abi.v1.Dependency + (*BlockMeta)(nil), // 2: abi.v1.BlockMeta + (*BlockTemplateOverride)(nil), // 3: abi.v1.BlockTemplateOverride + (*SystemTemplateMeta)(nil), // 4: abi.v1.SystemTemplateMeta + (*PageTemplateMeta)(nil), // 5: abi.v1.PageTemplateMeta + (*AdminPage)(nil), // 6: abi.v1.AdminPage + (*AiAction)(nil), // 7: abi.v1.AiAction + (*CssManifest)(nil), // 8: abi.v1.CssManifest + (*DirectoryExtensions)(nil), // 9: abi.v1.DirectoryExtensions + (*BadgeLabel)(nil), // 10: abi.v1.BadgeLabel + (*MasterPageDefinition)(nil), // 11: abi.v1.MasterPageDefinition + (*MasterPageBlock)(nil), // 12: abi.v1.MasterPageBlock + (*CoreServiceBinding)(nil), // 13: abi.v1.CoreServiceBinding + nil, // 14: abi.v1.PluginManifest.RbacMethodRolesEntry + nil, // 15: abi.v1.CssManifest.NpmPackagesEntry + nil, // 16: abi.v1.DirectoryExtensions.BadgeLabelsEntry + nil, // 17: abi.v1.CoreServiceBinding.MethodRolesEntry +} +var file_v1_manifest_proto_depIdxs = []int32{ + 1, // 0: abi.v1.PluginManifest.dependencies:type_name -> abi.v1.Dependency + 2, // 1: abi.v1.PluginManifest.blocks:type_name -> abi.v1.BlockMeta + 3, // 2: abi.v1.PluginManifest.block_template_overrides:type_name -> abi.v1.BlockTemplateOverride + 4, // 3: abi.v1.PluginManifest.system_templates:type_name -> abi.v1.SystemTemplateMeta + 5, // 4: abi.v1.PluginManifest.page_templates:type_name -> abi.v1.PageTemplateMeta + 6, // 5: abi.v1.PluginManifest.admin_pages:type_name -> abi.v1.AdminPage + 11, // 6: abi.v1.PluginManifest.master_pages:type_name -> abi.v1.MasterPageDefinition + 7, // 7: abi.v1.PluginManifest.ai_actions:type_name -> abi.v1.AiAction + 14, // 8: abi.v1.PluginManifest.rbac_method_roles:type_name -> abi.v1.PluginManifest.RbacMethodRolesEntry + 8, // 9: abi.v1.PluginManifest.css_manifest:type_name -> abi.v1.CssManifest + 9, // 10: abi.v1.PluginManifest.directory_extensions:type_name -> abi.v1.DirectoryExtensions + 13, // 11: abi.v1.PluginManifest.core_service_bindings:type_name -> abi.v1.CoreServiceBinding + 15, // 12: abi.v1.CssManifest.npm_packages:type_name -> abi.v1.CssManifest.NpmPackagesEntry + 16, // 13: abi.v1.DirectoryExtensions.badge_labels:type_name -> abi.v1.DirectoryExtensions.BadgeLabelsEntry + 12, // 14: abi.v1.MasterPageDefinition.blocks:type_name -> abi.v1.MasterPageBlock + 17, // 15: abi.v1.CoreServiceBinding.method_roles:type_name -> abi.v1.CoreServiceBinding.MethodRolesEntry + 10, // 16: abi.v1.DirectoryExtensions.BadgeLabelsEntry.value:type_name -> abi.v1.BadgeLabel + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name +} + +func init() { file_v1_manifest_proto_init() } +func file_v1_manifest_proto_init() { + if File_v1_manifest_proto != nil { + return + } + file_v1_manifest_proto_msgTypes[12].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_manifest_proto_rawDesc), len(file_v1_manifest_proto_rawDesc)), + NumEnums: 0, + NumMessages: 18, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_manifest_proto_goTypes, + DependencyIndexes: file_v1_manifest_proto_depIdxs, + MessageInfos: file_v1_manifest_proto_msgTypes, + }.Build() + File_v1_manifest_proto = out.File + file_v1_manifest_proto_goTypes = nil + file_v1_manifest_proto_depIdxs = nil +} diff --git a/abi/v1/render.pb.go b/abi/v1/render.pb.go new file mode 100644 index 0000000..8238edb --- /dev/null +++ b/abi/v1/render.pb.go @@ -0,0 +1,2006 @@ +// render.proto — block/template render payloads and the explicit +// render-context envelope (WO-WZ-001). +// +// RenderContext serializes every value blocks currently read from ctx via +// core/blocks/context.go. Function-valued context entries (SlotRenderer, +// MediaResolver, EmbedResolver, the generic Queries value) cannot cross the +// wire; see core/docs/wasm-abi.md for how each one maps. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: v1/render.proto + +package abiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RenderBlockRequest renders one block (blocks.BlockFunc). +type RenderBlockRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BlockKey string `protobuf:"bytes,1,opt,name=block_key,json=blockKey,proto3" json:"block_key,omitempty"` + // JSON encoding of the block's content map. + ContentJson []byte `protobuf:"bytes,2,opt,name=content_json,json=contentJson,proto3" json:"content_json,omitempty"` + RenderContext *RenderContext `protobuf:"bytes,3,opt,name=render_context,json=renderContext,proto3" json:"render_context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderBlockRequest) Reset() { + *x = RenderBlockRequest{} + mi := &file_v1_render_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderBlockRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderBlockRequest) ProtoMessage() {} + +func (x *RenderBlockRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderBlockRequest.ProtoReflect.Descriptor instead. +func (*RenderBlockRequest) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{0} +} + +func (x *RenderBlockRequest) GetBlockKey() string { + if x != nil { + return x.BlockKey + } + return "" +} + +func (x *RenderBlockRequest) GetContentJson() []byte { + if x != nil { + return x.ContentJson + } + return nil +} + +func (x *RenderBlockRequest) GetRenderContext() *RenderContext { + if x != nil { + return x.RenderContext + } + return nil +} + +type RenderBlockResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Final rendered HTML for a plain (non-template) block. Ignored when + // `powered` is set. + Html string `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"` + // Set when the block is "powered": instead of final HTML, the block hands + // back a template string + data map (blocks.PoweredBlock) and the HOST + // renders it (pongo2/ninjatpl, host-side) AFTER this RENDER_BLOCK call has + // returned. Because the block-invoke has already returned, the guest + // instance is free, so any plugin tag/filter the template hits (RENDER_TAG / + // APPLY_FILTER) is a fresh invoke — no re-entrancy. A singular message field + // has explicit presence: nil (GetPowered() == nil) means a plain HTML block. + Powered *PoweredBlock `protobuf:"bytes,2,opt,name=powered,proto3" json:"powered,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderBlockResponse) Reset() { + *x = RenderBlockResponse{} + mi := &file_v1_render_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderBlockResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderBlockResponse) ProtoMessage() {} + +func (x *RenderBlockResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderBlockResponse.ProtoReflect.Descriptor instead. +func (*RenderBlockResponse) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{1} +} + +func (x *RenderBlockResponse) GetHtml() string { + if x != nil { + return x.Html + } + return "" +} + +func (x *RenderBlockResponse) GetPowered() *PoweredBlock { + if x != nil { + return x.Powered + } + return nil +} + +// PoweredBlock is a template-backed block result. The host renders `template` +// with `data_json` as the pongo2 data map. See core/docs/wasm-abi.md +// §"Powered blocks (render as a host capability)". +type PoweredBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + // ninjatpl/pongo2 template source the host renders host-side. + Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` + // JSON encoding of the template data map (map[string]any). + DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PoweredBlock) Reset() { + *x = PoweredBlock{} + mi := &file_v1_render_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PoweredBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PoweredBlock) ProtoMessage() {} + +func (x *PoweredBlock) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PoweredBlock.ProtoReflect.Descriptor instead. +func (*PoweredBlock) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{2} +} + +func (x *PoweredBlock) GetTemplate() string { + if x != nil { + return x.Template + } + return "" +} + +func (x *PoweredBlock) GetDataJson() []byte { + if x != nil { + return x.DataJson + } + return nil +} + +// RenderTemplateRequest renders one template (templates.TemplateFunc). +type RenderTemplateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TemplateKey string `protobuf:"bytes,1,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` + // JSON encoding of the template's doc map (the page document). + DocJson []byte `protobuf:"bytes,2,opt,name=doc_json,json=docJson,proto3" json:"doc_json,omitempty"` + RenderContext *RenderContext `protobuf:"bytes,3,opt,name=render_context,json=renderContext,proto3" json:"render_context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderTemplateRequest) Reset() { + *x = RenderTemplateRequest{} + mi := &file_v1_render_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderTemplateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderTemplateRequest) ProtoMessage() {} + +func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderTemplateRequest.ProtoReflect.Descriptor instead. +func (*RenderTemplateRequest) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{3} +} + +func (x *RenderTemplateRequest) GetTemplateKey() string { + if x != nil { + return x.TemplateKey + } + return "" +} + +func (x *RenderTemplateRequest) GetDocJson() []byte { + if x != nil { + return x.DocJson + } + return nil +} + +func (x *RenderTemplateRequest) GetRenderContext() *RenderContext { + if x != nil { + return x.RenderContext + } + return nil +} + +type RenderTemplateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Html []byte `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderTemplateResponse) Reset() { + *x = RenderTemplateResponse{} + mi := &file_v1_render_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderTemplateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderTemplateResponse) ProtoMessage() {} + +func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderTemplateResponse.ProtoReflect.Descriptor instead. +func (*RenderTemplateResponse) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{4} +} + +func (x *RenderTemplateResponse) GetHtml() []byte { + if x != nil { + return x.Html + } + return nil +} + +// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG). +// The host wires one pongo2 tag per manifest.declared_tags name; when its +// engine hits that tag while rendering a powered block, it calls back here. +// Re-entrancy-free: the originating RENDER_BLOCK has already returned. +type RenderTagRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TagName string `protobuf:"bytes,1,opt,name=tag_name,json=tagName,proto3" json:"tag_name,omitempty"` + // JSON encoding of the tag's parsed arguments (map[string]any). + ArgsJson []byte `protobuf:"bytes,2,opt,name=args_json,json=argsJson,proto3" json:"args_json,omitempty"` + // The active render context (same envelope RENDER_BLOCK carries), so the + // tag fn can read page/post/request state via blocks.Get*. + RenderContext *RenderContext `protobuf:"bytes,3,opt,name=render_context,json=renderContext,proto3" json:"render_context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderTagRequest) Reset() { + *x = RenderTagRequest{} + mi := &file_v1_render_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderTagRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderTagRequest) ProtoMessage() {} + +func (x *RenderTagRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderTagRequest.ProtoReflect.Descriptor instead. +func (*RenderTagRequest) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{5} +} + +func (x *RenderTagRequest) GetTagName() string { + if x != nil { + return x.TagName + } + return "" +} + +func (x *RenderTagRequest) GetArgsJson() []byte { + if x != nil { + return x.ArgsJson + } + return nil +} + +func (x *RenderTagRequest) GetRenderContext() *RenderContext { + if x != nil { + return x.RenderContext + } + return nil +} + +// RenderTagResponse returns the tag's rendered HTML. A non-empty `error` +// carries the tag fn's returned error (distinct from an AbiError, which is +// reserved for transport/dispatch/panic failures). +type RenderTagResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Html string `protobuf:"bytes,1,opt,name=html,proto3" json:"html,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderTagResponse) Reset() { + *x = RenderTagResponse{} + mi := &file_v1_render_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderTagResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderTagResponse) ProtoMessage() {} + +func (x *RenderTagResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderTagResponse.ProtoReflect.Descriptor instead. +func (*RenderTagResponse) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{6} +} + +func (x *RenderTagResponse) GetHtml() string { + if x != nil { + return x.Html + } + return "" +} + +func (x *RenderTagResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// ApplyFilterRequest invokes a plugin-declared template filter +// (HOOK_APPLY_FILTER). The host wires one pongo2 filter per +// manifest.declared_filters name. +type ApplyFilterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + FilterName string `protobuf:"bytes,1,opt,name=filter_name,json=filterName,proto3" json:"filter_name,omitempty"` + // The value the filter is applied to (pongo2 passes strings). + Input string `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` + // JSON encoding of the filter's arguments (map[string]any). + ArgsJson []byte `protobuf:"bytes,3,opt,name=args_json,json=argsJson,proto3" json:"args_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyFilterRequest) Reset() { + *x = ApplyFilterRequest{} + mi := &file_v1_render_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyFilterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyFilterRequest) ProtoMessage() {} + +func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyFilterRequest.ProtoReflect.Descriptor instead. +func (*ApplyFilterRequest) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{7} +} + +func (x *ApplyFilterRequest) GetFilterName() string { + if x != nil { + return x.FilterName + } + return "" +} + +func (x *ApplyFilterRequest) GetInput() string { + if x != nil { + return x.Input + } + return "" +} + +func (x *ApplyFilterRequest) GetArgsJson() []byte { + if x != nil { + return x.ArgsJson + } + return nil +} + +// ApplyFilterResponse returns the filtered output. A non-empty `error` +// carries the filter fn's returned error. +type ApplyFilterResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyFilterResponse) Reset() { + *x = ApplyFilterResponse{} + mi := &file_v1_render_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyFilterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyFilterResponse) ProtoMessage() {} + +func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyFilterResponse.ProtoReflect.Descriptor instead. +func (*ApplyFilterResponse) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{8} +} + +func (x *ApplyFilterResponse) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +func (x *ApplyFilterResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// RenderContext is the explicit envelope of the context values enumerated +// from core/blocks/context.go. Absent sub-messages mean the corresponding +// ctx value was not set (the getters return nil / zero values). +type RenderContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + // blocks.GetRequest — subset of *http.Request that render code reads. + Request *RequestInfo `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"` + // blocks.GetBlockContext — the pongo2 template data struct. + BlockContext *BlockContext `protobuf:"bytes,2,opt,name=block_context,json=blockContext,proto3" json:"block_context,omitempty"` + // blocks.GetTemplateKey. + TemplateKey string `protobuf:"bytes,3,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` + // blocks.GetCurrentPage. + Page *PageContext `protobuf:"bytes,4,opt,name=page,proto3" json:"page,omitempty"` + // blocks.GetCurrentBlogPost. + Post *PostContext `protobuf:"bytes,5,opt,name=post,proto3" json:"post,omitempty"` + // blocks.GetCurrentAuthor. + Author *AuthorContext `protobuf:"bytes,6,opt,name=author,proto3" json:"author,omitempty"` + // blocks.GetCurrentCategory. + Category *CategoryContext `protobuf:"bytes,7,opt,name=category,proto3" json:"category,omitempty"` + // blocks.GetMasterPage; presence doubles as IsMasterPageContext. + MasterPage *MasterPageContext `protobuf:"bytes,8,opt,name=master_page,json=masterPage,proto3" json:"master_page,omitempty"` + // blocks.GetRequestedPath (404 pages). + RequestedPath string `protobuf:"bytes,9,opt,name=requested_path,json=requestedPath,proto3" json:"requested_path,omitempty"` + // blocks.GetInjectedSlots (master page rendering). + InjectedSlots map[string]string `protobuf:"bytes,10,rep,name=injected_slots,json=injectedSlots,proto3" json:"injected_slots,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // blocks.IsEditor. + IsEditor bool `protobuf:"varint,11,opt,name=is_editor,json=isEditor,proto3" json:"is_editor,omitempty"` + // blocks.GetExpectedSlots. + ExpectedSlots []string `protobuf:"bytes,12,rep,name=expected_slots,json=expectedSlots,proto3" json:"expected_slots,omitempty"` + // blocks.GetBlockID (UUID; empty for uuid.Nil). + BlockId string `protobuf:"bytes,13,opt,name=block_id,json=blockId,proto3" json:"block_id,omitempty"` + // blocks.GetCurrentPageID (UUID; empty for uuid.Nil). + CurrentPageId string `protobuf:"bytes,14,opt,name=current_page_id,json=currentPageId,proto3" json:"current_page_id,omitempty"` + // blocks.GetHumanProofBanner. + HumanProofBanner *HumanProofBanner `protobuf:"bytes,15,opt,name=human_proof_banner,json=humanProofBanner,proto3" json:"human_proof_banner,omitempty"` + // blocks.GetDetailRow. + DetailRow *DetailRow `protobuf:"bytes,16,opt,name=detail_row,json=detailRow,proto3" json:"detail_row,omitempty"` + // Active theme variables, JSON-encoded. + ThemeJson []byte `protobuf:"bytes,17,opt,name=theme_json,json=themeJson,proto3" json:"theme_json,omitempty"` + // Site locale (BCP 47). + Locale string `protobuf:"bytes,18,opt,name=locale,proto3" json:"locale,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenderContext) Reset() { + *x = RenderContext{} + mi := &file_v1_render_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenderContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderContext) ProtoMessage() {} + +func (x *RenderContext) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderContext.ProtoReflect.Descriptor instead. +func (*RenderContext) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{9} +} + +func (x *RenderContext) GetRequest() *RequestInfo { + if x != nil { + return x.Request + } + return nil +} + +func (x *RenderContext) GetBlockContext() *BlockContext { + if x != nil { + return x.BlockContext + } + return nil +} + +func (x *RenderContext) GetTemplateKey() string { + if x != nil { + return x.TemplateKey + } + return "" +} + +func (x *RenderContext) GetPage() *PageContext { + if x != nil { + return x.Page + } + return nil +} + +func (x *RenderContext) GetPost() *PostContext { + if x != nil { + return x.Post + } + return nil +} + +func (x *RenderContext) GetAuthor() *AuthorContext { + if x != nil { + return x.Author + } + return nil +} + +func (x *RenderContext) GetCategory() *CategoryContext { + if x != nil { + return x.Category + } + return nil +} + +func (x *RenderContext) GetMasterPage() *MasterPageContext { + if x != nil { + return x.MasterPage + } + return nil +} + +func (x *RenderContext) GetRequestedPath() string { + if x != nil { + return x.RequestedPath + } + return "" +} + +func (x *RenderContext) GetInjectedSlots() map[string]string { + if x != nil { + return x.InjectedSlots + } + return nil +} + +func (x *RenderContext) GetIsEditor() bool { + if x != nil { + return x.IsEditor + } + return false +} + +func (x *RenderContext) GetExpectedSlots() []string { + if x != nil { + return x.ExpectedSlots + } + return nil +} + +func (x *RenderContext) GetBlockId() string { + if x != nil { + return x.BlockId + } + return "" +} + +func (x *RenderContext) GetCurrentPageId() string { + if x != nil { + return x.CurrentPageId + } + return "" +} + +func (x *RenderContext) GetHumanProofBanner() *HumanProofBanner { + if x != nil { + return x.HumanProofBanner + } + return nil +} + +func (x *RenderContext) GetDetailRow() *DetailRow { + if x != nil { + return x.DetailRow + } + return nil +} + +func (x *RenderContext) GetThemeJson() []byte { + if x != nil { + return x.ThemeJson + } + return nil +} + +func (x *RenderContext) GetLocale() string { + if x != nil { + return x.Locale + } + return "" +} + +// RequestInfo carries the request subset render code reads from +// blocks.GetRequest (single-valued header map, matching BlockContext). +type RequestInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Host string `protobuf:"bytes,4,opt,name=host,proto3" json:"host,omitempty"` + RawQuery string `protobuf:"bytes,5,opt,name=raw_query,json=rawQuery,proto3" json:"raw_query,omitempty"` + Headers map[string]string `protobuf:"bytes,6,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Cookies map[string]string `protobuf:"bytes,7,rep,name=cookies,proto3" json:"cookies,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + RemoteIp string `protobuf:"bytes,8,opt,name=remote_ip,json=remoteIp,proto3" json:"remote_ip,omitempty"` + Referrer string `protobuf:"bytes,9,opt,name=referrer,proto3" json:"referrer,omitempty"` + UserAgent string `protobuf:"bytes,10,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestInfo) Reset() { + *x = RequestInfo{} + mi := &file_v1_render_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestInfo) ProtoMessage() {} + +func (x *RequestInfo) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestInfo.ProtoReflect.Descriptor instead. +func (*RequestInfo) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{10} +} + +func (x *RequestInfo) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *RequestInfo) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *RequestInfo) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *RequestInfo) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *RequestInfo) GetRawQuery() string { + if x != nil { + return x.RawQuery + } + return "" +} + +func (x *RequestInfo) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + +func (x *RequestInfo) GetCookies() map[string]string { + if x != nil { + return x.Cookies + } + return nil +} + +func (x *RequestInfo) GetRemoteIp() string { + if x != nil { + return x.RemoteIp + } + return "" +} + +func (x *RequestInfo) GetReferrer() string { + if x != nil { + return x.Referrer + } + return "" +} + +func (x *RequestInfo) GetUserAgent() string { + if x != nil { + return x.UserAgent + } + return "" +} + +// PageContext mirrors blocks.PageContext. +type PageContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + PostType string `protobuf:"bytes,4,opt,name=post_type,json=postType,proto3" json:"post_type,omitempty"` // "page", "post", "master", "system" + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "published", "draft", "scheduled" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PageContext) Reset() { + *x = PageContext{} + mi := &file_v1_render_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PageContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PageContext) ProtoMessage() {} + +func (x *PageContext) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PageContext.ProtoReflect.Descriptor instead. +func (*PageContext) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{11} +} + +func (x *PageContext) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *PageContext) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *PageContext) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *PageContext) GetPostType() string { + if x != nil { + return x.PostType + } + return "" +} + +func (x *PageContext) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +// PostContext mirrors blocks.PostContext. +type PostContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + Excerpt string `protobuf:"bytes,4,opt,name=excerpt,proto3" json:"excerpt,omitempty"` + FeaturedImageUrl string `protobuf:"bytes,5,opt,name=featured_image_url,json=featuredImageUrl,proto3" json:"featured_image_url,omitempty"` + AuthorId string `protobuf:"bytes,6,opt,name=author_id,json=authorId,proto3" json:"author_id,omitempty"` // UUID + PublishedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=published_at,json=publishedAt,proto3" json:"published_at,omitempty"` + ReadingTime int32 `protobuf:"varint,8,opt,name=reading_time,json=readingTime,proto3" json:"reading_time,omitempty"` + IsFeatured bool `protobuf:"varint,9,opt,name=is_featured,json=isFeatured,proto3" json:"is_featured,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PostContext) Reset() { + *x = PostContext{} + mi := &file_v1_render_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PostContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PostContext) ProtoMessage() {} + +func (x *PostContext) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PostContext.ProtoReflect.Descriptor instead. +func (*PostContext) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{12} +} + +func (x *PostContext) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *PostContext) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *PostContext) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *PostContext) GetExcerpt() string { + if x != nil { + return x.Excerpt + } + return "" +} + +func (x *PostContext) GetFeaturedImageUrl() string { + if x != nil { + return x.FeaturedImageUrl + } + return "" +} + +func (x *PostContext) GetAuthorId() string { + if x != nil { + return x.AuthorId + } + return "" +} + +func (x *PostContext) GetPublishedAt() *timestamppb.Timestamp { + if x != nil { + return x.PublishedAt + } + return nil +} + +func (x *PostContext) GetReadingTime() int32 { + if x != nil { + return x.ReadingTime + } + return 0 +} + +func (x *PostContext) GetIsFeatured() bool { + if x != nil { + return x.IsFeatured + } + return false +} + +// AuthorContext mirrors blocks.AuthorContext. +type AuthorContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"` + Bio string `protobuf:"bytes,4,opt,name=bio,proto3" json:"bio,omitempty"` + AvatarUrl string `protobuf:"bytes,5,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorContext) Reset() { + *x = AuthorContext{} + mi := &file_v1_render_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorContext) ProtoMessage() {} + +func (x *AuthorContext) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorContext.ProtoReflect.Descriptor instead. +func (*AuthorContext) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{13} +} + +func (x *AuthorContext) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AuthorContext) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AuthorContext) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *AuthorContext) GetBio() string { + if x != nil { + return x.Bio + } + return "" +} + +func (x *AuthorContext) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +// CategoryContext mirrors blocks.CategoryContext. +type CategoryContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CategoryContext) Reset() { + *x = CategoryContext{} + mi := &file_v1_render_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CategoryContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CategoryContext) ProtoMessage() {} + +func (x *CategoryContext) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CategoryContext.ProtoReflect.Descriptor instead. +func (*CategoryContext) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{14} +} + +func (x *CategoryContext) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *CategoryContext) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CategoryContext) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +// MasterPageContext mirrors blocks.MasterPageContext. +type MasterPageContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MasterPageContext) Reset() { + *x = MasterPageContext{} + mi := &file_v1_render_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MasterPageContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MasterPageContext) ProtoMessage() {} + +func (x *MasterPageContext) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MasterPageContext.ProtoReflect.Descriptor instead. +func (*MasterPageContext) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{15} +} + +func (x *MasterPageContext) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *MasterPageContext) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *MasterPageContext) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +// HumanProofBanner mirrors blocks.HumanProofBannerData. +type HumanProofBanner struct { + state protoimpl.MessageState `protogen:"open.v1"` + ActiveTimeMinutes int32 `protobuf:"varint,1,opt,name=active_time_minutes,json=activeTimeMinutes,proto3" json:"active_time_minutes,omitempty"` + KeystrokeCount int32 `protobuf:"varint,2,opt,name=keystroke_count,json=keystrokeCount,proto3" json:"keystroke_count,omitempty"` + SessionCount int32 `protobuf:"varint,3,opt,name=session_count,json=sessionCount,proto3" json:"session_count,omitempty"` + PostSlug string `protobuf:"bytes,4,opt,name=post_slug,json=postSlug,proto3" json:"post_slug,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HumanProofBanner) Reset() { + *x = HumanProofBanner{} + mi := &file_v1_render_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HumanProofBanner) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HumanProofBanner) ProtoMessage() {} + +func (x *HumanProofBanner) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HumanProofBanner.ProtoReflect.Descriptor instead. +func (*HumanProofBanner) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{16} +} + +func (x *HumanProofBanner) GetActiveTimeMinutes() int32 { + if x != nil { + return x.ActiveTimeMinutes + } + return 0 +} + +func (x *HumanProofBanner) GetKeystrokeCount() int32 { + if x != nil { + return x.KeystrokeCount + } + return 0 +} + +func (x *HumanProofBanner) GetSessionCount() int32 { + if x != nil { + return x.SessionCount + } + return 0 +} + +func (x *HumanProofBanner) GetPostSlug() string { + if x != nil { + return x.PostSlug + } + return "" +} + +// DetailRow mirrors blocks.DetailRowInfo. +type DetailRow struct { + state protoimpl.MessageState `protogen:"open.v1"` + TableId string `protobuf:"bytes,1,opt,name=table_id,json=tableId,proto3" json:"table_id,omitempty"` + RowId string `protobuf:"bytes,2,opt,name=row_id,json=rowId,proto3" json:"row_id,omitempty"` + // JSON encoding of the row data map. + DataJson []byte `protobuf:"bytes,3,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetailRow) Reset() { + *x = DetailRow{} + mi := &file_v1_render_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetailRow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetailRow) ProtoMessage() {} + +func (x *DetailRow) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DetailRow.ProtoReflect.Descriptor instead. +func (*DetailRow) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{17} +} + +func (x *DetailRow) GetTableId() string { + if x != nil { + return x.TableId + } + return "" +} + +func (x *DetailRow) GetRowId() string { + if x != nil { + return x.RowId + } + return "" +} + +func (x *DetailRow) GetDataJson() []byte { + if x != nil { + return x.DataJson + } + return nil +} + +// BlockContext mirrors blocks.BlockContext (the pongo2 data struct) 1:1. +// map[string]any fields travel as JSON bytes. +type BlockContext struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"` + PageId string `protobuf:"bytes,4,opt,name=page_id,json=pageId,proto3" json:"page_id,omitempty"` + PageTitle string `protobuf:"bytes,5,opt,name=page_title,json=pageTitle,proto3" json:"page_title,omitempty"` + TemplateKey string `protobuf:"bytes,6,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` + IsEditor bool `protobuf:"varint,7,opt,name=is_editor,json=isEditor,proto3" json:"is_editor,omitempty"` + Timestamp int64 `protobuf:"varint,8,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Now *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=now,proto3" json:"now,omitempty"` + IsLoggedIn bool `protobuf:"varint,10,opt,name=is_logged_in,json=isLoggedIn,proto3" json:"is_logged_in,omitempty"` + UserId string `protobuf:"bytes,11,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + UserEmail string `protobuf:"bytes,12,opt,name=user_email,json=userEmail,proto3" json:"user_email,omitempty"` + UserRole string `protobuf:"bytes,13,opt,name=user_role,json=userRole,proto3" json:"user_role,omitempty"` + Method string `protobuf:"bytes,14,opt,name=method,proto3" json:"method,omitempty"` + Host string `protobuf:"bytes,15,opt,name=host,proto3" json:"host,omitempty"` + Query map[string]string `protobuf:"bytes,16,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Referrer string `protobuf:"bytes,17,opt,name=referrer,proto3" json:"referrer,omitempty"` + UserAgent string `protobuf:"bytes,18,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"` + Ip string `protobuf:"bytes,19,opt,name=ip,proto3" json:"ip,omitempty"` + Cookies map[string]string `protobuf:"bytes,20,rep,name=cookies,proto3" json:"cookies,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Headers map[string]string `protobuf:"bytes,21,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Country string `protobuf:"bytes,22,opt,name=country,proto3" json:"country,omitempty"` + City string `protobuf:"bytes,23,opt,name=city,proto3" json:"city,omitempty"` + Timezone string `protobuf:"bytes,24,opt,name=timezone,proto3" json:"timezone,omitempty"` + CurrentAuthorJson []byte `protobuf:"bytes,25,opt,name=current_author_json,json=currentAuthorJson,proto3" json:"current_author_json,omitempty"` + CurrentPostJson []byte `protobuf:"bytes,26,opt,name=current_post_json,json=currentPostJson,proto3" json:"current_post_json,omitempty"` + CurrentCategoryJson []byte `protobuf:"bytes,27,opt,name=current_category_json,json=currentCategoryJson,proto3" json:"current_category_json,omitempty"` + SiteJson []byte `protobuf:"bytes,28,opt,name=site_json,json=siteJson,proto3" json:"site_json,omitempty"` + IsPublicLoggedIn bool `protobuf:"varint,29,opt,name=is_public_logged_in,json=isPublicLoggedIn,proto3" json:"is_public_logged_in,omitempty"` + PublicUserId string `protobuf:"bytes,30,opt,name=public_user_id,json=publicUserId,proto3" json:"public_user_id,omitempty"` + PublicUsername string `protobuf:"bytes,31,opt,name=public_username,json=publicUsername,proto3" json:"public_username,omitempty"` + PublicDisplayName string `protobuf:"bytes,32,opt,name=public_display_name,json=publicDisplayName,proto3" json:"public_display_name,omitempty"` + PublicEmailVerified bool `protobuf:"varint,33,opt,name=public_email_verified,json=publicEmailVerified,proto3" json:"public_email_verified,omitempty"` + DetailRowId string `protobuf:"bytes,34,opt,name=detail_row_id,json=detailRowId,proto3" json:"detail_row_id,omitempty"` + DetailTableId string `protobuf:"bytes,35,opt,name=detail_table_id,json=detailTableId,proto3" json:"detail_table_id,omitempty"` + DetailRowDataJson []byte `protobuf:"bytes,36,opt,name=detail_row_data_json,json=detailRowDataJson,proto3" json:"detail_row_data_json,omitempty"` + BlogIndexUrl string `protobuf:"bytes,37,opt,name=blog_index_url,json=blogIndexUrl,proto3" json:"blog_index_url,omitempty"` + CategoryPageUrl string `protobuf:"bytes,38,opt,name=category_page_url,json=categoryPageUrl,proto3" json:"category_page_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BlockContext) Reset() { + *x = BlockContext{} + mi := &file_v1_render_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlockContext) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlockContext) ProtoMessage() {} + +func (x *BlockContext) ProtoReflect() protoreflect.Message { + mi := &file_v1_render_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlockContext.ProtoReflect.Descriptor instead. +func (*BlockContext) Descriptor() ([]byte, []int) { + return file_v1_render_proto_rawDescGZIP(), []int{18} +} + +func (x *BlockContext) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *BlockContext) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *BlockContext) GetSlug() string { + if x != nil { + return x.Slug + } + return "" +} + +func (x *BlockContext) GetPageId() string { + if x != nil { + return x.PageId + } + return "" +} + +func (x *BlockContext) GetPageTitle() string { + if x != nil { + return x.PageTitle + } + return "" +} + +func (x *BlockContext) GetTemplateKey() string { + if x != nil { + return x.TemplateKey + } + return "" +} + +func (x *BlockContext) GetIsEditor() bool { + if x != nil { + return x.IsEditor + } + return false +} + +func (x *BlockContext) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *BlockContext) GetNow() *timestamppb.Timestamp { + if x != nil { + return x.Now + } + return nil +} + +func (x *BlockContext) GetIsLoggedIn() bool { + if x != nil { + return x.IsLoggedIn + } + return false +} + +func (x *BlockContext) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *BlockContext) GetUserEmail() string { + if x != nil { + return x.UserEmail + } + return "" +} + +func (x *BlockContext) GetUserRole() string { + if x != nil { + return x.UserRole + } + return "" +} + +func (x *BlockContext) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *BlockContext) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *BlockContext) GetQuery() map[string]string { + if x != nil { + return x.Query + } + return nil +} + +func (x *BlockContext) GetReferrer() string { + if x != nil { + return x.Referrer + } + return "" +} + +func (x *BlockContext) GetUserAgent() string { + if x != nil { + return x.UserAgent + } + return "" +} + +func (x *BlockContext) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *BlockContext) GetCookies() map[string]string { + if x != nil { + return x.Cookies + } + return nil +} + +func (x *BlockContext) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + +func (x *BlockContext) GetCountry() string { + if x != nil { + return x.Country + } + return "" +} + +func (x *BlockContext) GetCity() string { + if x != nil { + return x.City + } + return "" +} + +func (x *BlockContext) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + +func (x *BlockContext) GetCurrentAuthorJson() []byte { + if x != nil { + return x.CurrentAuthorJson + } + return nil +} + +func (x *BlockContext) GetCurrentPostJson() []byte { + if x != nil { + return x.CurrentPostJson + } + return nil +} + +func (x *BlockContext) GetCurrentCategoryJson() []byte { + if x != nil { + return x.CurrentCategoryJson + } + return nil +} + +func (x *BlockContext) GetSiteJson() []byte { + if x != nil { + return x.SiteJson + } + return nil +} + +func (x *BlockContext) GetIsPublicLoggedIn() bool { + if x != nil { + return x.IsPublicLoggedIn + } + return false +} + +func (x *BlockContext) GetPublicUserId() string { + if x != nil { + return x.PublicUserId + } + return "" +} + +func (x *BlockContext) GetPublicUsername() string { + if x != nil { + return x.PublicUsername + } + return "" +} + +func (x *BlockContext) GetPublicDisplayName() string { + if x != nil { + return x.PublicDisplayName + } + return "" +} + +func (x *BlockContext) GetPublicEmailVerified() bool { + if x != nil { + return x.PublicEmailVerified + } + return false +} + +func (x *BlockContext) GetDetailRowId() string { + if x != nil { + return x.DetailRowId + } + return "" +} + +func (x *BlockContext) GetDetailTableId() string { + if x != nil { + return x.DetailTableId + } + return "" +} + +func (x *BlockContext) GetDetailRowDataJson() []byte { + if x != nil { + return x.DetailRowDataJson + } + return nil +} + +func (x *BlockContext) GetBlogIndexUrl() string { + if x != nil { + return x.BlogIndexUrl + } + return "" +} + +func (x *BlockContext) GetCategoryPageUrl() string { + if x != nil { + return x.CategoryPageUrl + } + return "" +} + +var File_v1_render_proto protoreflect.FileDescriptor + +const file_v1_render_proto_rawDesc = "" + + "\n" + + "\x0fv1/render.proto\x12\x06abi.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x92\x01\n" + + "\x12RenderBlockRequest\x12\x1b\n" + + "\tblock_key\x18\x01 \x01(\tR\bblockKey\x12!\n" + + "\fcontent_json\x18\x02 \x01(\fR\vcontentJson\x12<\n" + + "\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"Y\n" + + "\x13RenderBlockResponse\x12\x12\n" + + "\x04html\x18\x01 \x01(\tR\x04html\x12.\n" + + "\apowered\x18\x02 \x01(\v2\x14.abi.v1.PoweredBlockR\apowered\"G\n" + + "\fPoweredBlock\x12\x1a\n" + + "\btemplate\x18\x01 \x01(\tR\btemplate\x12\x1b\n" + + "\tdata_json\x18\x02 \x01(\fR\bdataJson\"\x93\x01\n" + + "\x15RenderTemplateRequest\x12!\n" + + "\ftemplate_key\x18\x01 \x01(\tR\vtemplateKey\x12\x19\n" + + "\bdoc_json\x18\x02 \x01(\fR\adocJson\x12<\n" + + "\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\",\n" + + "\x16RenderTemplateResponse\x12\x12\n" + + "\x04html\x18\x01 \x01(\fR\x04html\"\x88\x01\n" + + "\x10RenderTagRequest\x12\x19\n" + + "\btag_name\x18\x01 \x01(\tR\atagName\x12\x1b\n" + + "\targs_json\x18\x02 \x01(\fR\bargsJson\x12<\n" + + "\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"=\n" + + "\x11RenderTagResponse\x12\x12\n" + + "\x04html\x18\x01 \x01(\tR\x04html\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\"h\n" + + "\x12ApplyFilterRequest\x12\x1f\n" + + "\vfilter_name\x18\x01 \x01(\tR\n" + + "filterName\x12\x14\n" + + "\x05input\x18\x02 \x01(\tR\x05input\x12\x1b\n" + + "\targs_json\x18\x03 \x01(\fR\bargsJson\"C\n" + + "\x13ApplyFilterResponse\x12\x16\n" + + "\x06output\x18\x01 \x01(\tR\x06output\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\"\x80\a\n" + + "\rRenderContext\x12-\n" + + "\arequest\x18\x01 \x01(\v2\x13.abi.v1.RequestInfoR\arequest\x129\n" + + "\rblock_context\x18\x02 \x01(\v2\x14.abi.v1.BlockContextR\fblockContext\x12!\n" + + "\ftemplate_key\x18\x03 \x01(\tR\vtemplateKey\x12'\n" + + "\x04page\x18\x04 \x01(\v2\x13.abi.v1.PageContextR\x04page\x12'\n" + + "\x04post\x18\x05 \x01(\v2\x13.abi.v1.PostContextR\x04post\x12-\n" + + "\x06author\x18\x06 \x01(\v2\x15.abi.v1.AuthorContextR\x06author\x123\n" + + "\bcategory\x18\a \x01(\v2\x17.abi.v1.CategoryContextR\bcategory\x12:\n" + + "\vmaster_page\x18\b \x01(\v2\x19.abi.v1.MasterPageContextR\n" + + "masterPage\x12%\n" + + "\x0erequested_path\x18\t \x01(\tR\rrequestedPath\x12O\n" + + "\x0einjected_slots\x18\n" + + " \x03(\v2(.abi.v1.RenderContext.InjectedSlotsEntryR\rinjectedSlots\x12\x1b\n" + + "\tis_editor\x18\v \x01(\bR\bisEditor\x12%\n" + + "\x0eexpected_slots\x18\f \x03(\tR\rexpectedSlots\x12\x19\n" + + "\bblock_id\x18\r \x01(\tR\ablockId\x12&\n" + + "\x0fcurrent_page_id\x18\x0e \x01(\tR\rcurrentPageId\x12F\n" + + "\x12human_proof_banner\x18\x0f \x01(\v2\x18.abi.v1.HumanProofBannerR\x10humanProofBanner\x120\n" + + "\n" + + "detail_row\x18\x10 \x01(\v2\x11.abi.v1.DetailRowR\tdetailRow\x12\x1d\n" + + "\n" + + "theme_json\x18\x11 \x01(\fR\tthemeJson\x12\x16\n" + + "\x06locale\x18\x12 \x01(\tR\x06locale\x1a@\n" + + "\x12InjectedSlotsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc4\x03\n" + + "\vRequestInfo\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x10\n" + + "\x03url\x18\x02 \x01(\tR\x03url\x12\x12\n" + + "\x04path\x18\x03 \x01(\tR\x04path\x12\x12\n" + + "\x04host\x18\x04 \x01(\tR\x04host\x12\x1b\n" + + "\traw_query\x18\x05 \x01(\tR\brawQuery\x12:\n" + + "\aheaders\x18\x06 \x03(\v2 .abi.v1.RequestInfo.HeadersEntryR\aheaders\x12:\n" + + "\acookies\x18\a \x03(\v2 .abi.v1.RequestInfo.CookiesEntryR\acookies\x12\x1b\n" + + "\tremote_ip\x18\b \x01(\tR\bremoteIp\x12\x1a\n" + + "\breferrer\x18\t \x01(\tR\breferrer\x12\x1d\n" + + "\n" + + "user_agent\x18\n" + + " \x01(\tR\tuserAgent\x1a:\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + + "\fCookiesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"|\n" + + "\vPageContext\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" + + "\tpost_type\x18\x04 \x01(\tR\bpostType\x12\x16\n" + + "\x06status\x18\x05 \x01(\tR\x06status\"\xaf\x02\n" + + "\vPostContext\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\x12\x18\n" + + "\aexcerpt\x18\x04 \x01(\tR\aexcerpt\x12,\n" + + "\x12featured_image_url\x18\x05 \x01(\tR\x10featuredImageUrl\x12\x1b\n" + + "\tauthor_id\x18\x06 \x01(\tR\bauthorId\x12=\n" + + "\fpublished_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\vpublishedAt\x12!\n" + + "\freading_time\x18\b \x01(\x05R\vreadingTime\x12\x1f\n" + + "\vis_featured\x18\t \x01(\bR\n" + + "isFeatured\"x\n" + + "\rAuthorContext\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04slug\x18\x03 \x01(\tR\x04slug\x12\x10\n" + + "\x03bio\x18\x04 \x01(\tR\x03bio\x12\x1d\n" + + "\n" + + "avatar_url\x18\x05 \x01(\tR\tavatarUrl\"I\n" + + "\x0fCategoryContext\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" + + "\x04slug\x18\x03 \x01(\tR\x04slug\"M\n" + + "\x11MasterPageContext\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + + "\x05title\x18\x03 \x01(\tR\x05title\"\xad\x01\n" + + "\x10HumanProofBanner\x12.\n" + + "\x13active_time_minutes\x18\x01 \x01(\x05R\x11activeTimeMinutes\x12'\n" + + "\x0fkeystroke_count\x18\x02 \x01(\x05R\x0ekeystrokeCount\x12#\n" + + "\rsession_count\x18\x03 \x01(\x05R\fsessionCount\x12\x1b\n" + + "\tpost_slug\x18\x04 \x01(\tR\bpostSlug\"Z\n" + + "\tDetailRow\x12\x19\n" + + "\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" + + "\x06row_id\x18\x02 \x01(\tR\x05rowId\x12\x1b\n" + + "\tdata_json\x18\x03 \x01(\fR\bdataJson\"\x85\f\n" + + "\fBlockContext\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + + "\x04slug\x18\x03 \x01(\tR\x04slug\x12\x17\n" + + "\apage_id\x18\x04 \x01(\tR\x06pageId\x12\x1d\n" + + "\n" + + "page_title\x18\x05 \x01(\tR\tpageTitle\x12!\n" + + "\ftemplate_key\x18\x06 \x01(\tR\vtemplateKey\x12\x1b\n" + + "\tis_editor\x18\a \x01(\bR\bisEditor\x12\x1c\n" + + "\ttimestamp\x18\b \x01(\x03R\ttimestamp\x12,\n" + + "\x03now\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\x03now\x12 \n" + + "\fis_logged_in\x18\n" + + " \x01(\bR\n" + + "isLoggedIn\x12\x17\n" + + "\auser_id\x18\v \x01(\tR\x06userId\x12\x1d\n" + + "\n" + + "user_email\x18\f \x01(\tR\tuserEmail\x12\x1b\n" + + "\tuser_role\x18\r \x01(\tR\buserRole\x12\x16\n" + + "\x06method\x18\x0e \x01(\tR\x06method\x12\x12\n" + + "\x04host\x18\x0f \x01(\tR\x04host\x125\n" + + "\x05query\x18\x10 \x03(\v2\x1f.abi.v1.BlockContext.QueryEntryR\x05query\x12\x1a\n" + + "\breferrer\x18\x11 \x01(\tR\breferrer\x12\x1d\n" + + "\n" + + "user_agent\x18\x12 \x01(\tR\tuserAgent\x12\x0e\n" + + "\x02ip\x18\x13 \x01(\tR\x02ip\x12;\n" + + "\acookies\x18\x14 \x03(\v2!.abi.v1.BlockContext.CookiesEntryR\acookies\x12;\n" + + "\aheaders\x18\x15 \x03(\v2!.abi.v1.BlockContext.HeadersEntryR\aheaders\x12\x18\n" + + "\acountry\x18\x16 \x01(\tR\acountry\x12\x12\n" + + "\x04city\x18\x17 \x01(\tR\x04city\x12\x1a\n" + + "\btimezone\x18\x18 \x01(\tR\btimezone\x12.\n" + + "\x13current_author_json\x18\x19 \x01(\fR\x11currentAuthorJson\x12*\n" + + "\x11current_post_json\x18\x1a \x01(\fR\x0fcurrentPostJson\x122\n" + + "\x15current_category_json\x18\x1b \x01(\fR\x13currentCategoryJson\x12\x1b\n" + + "\tsite_json\x18\x1c \x01(\fR\bsiteJson\x12-\n" + + "\x13is_public_logged_in\x18\x1d \x01(\bR\x10isPublicLoggedIn\x12$\n" + + "\x0epublic_user_id\x18\x1e \x01(\tR\fpublicUserId\x12'\n" + + "\x0fpublic_username\x18\x1f \x01(\tR\x0epublicUsername\x12.\n" + + "\x13public_display_name\x18 \x01(\tR\x11publicDisplayName\x122\n" + + "\x15public_email_verified\x18! \x01(\bR\x13publicEmailVerified\x12\"\n" + + "\rdetail_row_id\x18\" \x01(\tR\vdetailRowId\x12&\n" + + "\x0fdetail_table_id\x18# \x01(\tR\rdetailTableId\x12/\n" + + "\x14detail_row_data_json\x18$ \x01(\fR\x11detailRowDataJson\x12$\n" + + "\x0eblog_index_url\x18% \x01(\tR\fblogIndexUrl\x12*\n" + + "\x11category_page_url\x18& \x01(\tR\x0fcategoryPageUrl\x1a8\n" + + "\n" + + "QueryEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + + "\fCookiesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B5Z3git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1b\x06proto3" + +var ( + file_v1_render_proto_rawDescOnce sync.Once + file_v1_render_proto_rawDescData []byte +) + +func file_v1_render_proto_rawDescGZIP() []byte { + file_v1_render_proto_rawDescOnce.Do(func() { + file_v1_render_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_render_proto_rawDesc), len(file_v1_render_proto_rawDesc))) + }) + return file_v1_render_proto_rawDescData +} + +var file_v1_render_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_v1_render_proto_goTypes = []any{ + (*RenderBlockRequest)(nil), // 0: abi.v1.RenderBlockRequest + (*RenderBlockResponse)(nil), // 1: abi.v1.RenderBlockResponse + (*PoweredBlock)(nil), // 2: abi.v1.PoweredBlock + (*RenderTemplateRequest)(nil), // 3: abi.v1.RenderTemplateRequest + (*RenderTemplateResponse)(nil), // 4: abi.v1.RenderTemplateResponse + (*RenderTagRequest)(nil), // 5: abi.v1.RenderTagRequest + (*RenderTagResponse)(nil), // 6: abi.v1.RenderTagResponse + (*ApplyFilterRequest)(nil), // 7: abi.v1.ApplyFilterRequest + (*ApplyFilterResponse)(nil), // 8: abi.v1.ApplyFilterResponse + (*RenderContext)(nil), // 9: abi.v1.RenderContext + (*RequestInfo)(nil), // 10: abi.v1.RequestInfo + (*PageContext)(nil), // 11: abi.v1.PageContext + (*PostContext)(nil), // 12: abi.v1.PostContext + (*AuthorContext)(nil), // 13: abi.v1.AuthorContext + (*CategoryContext)(nil), // 14: abi.v1.CategoryContext + (*MasterPageContext)(nil), // 15: abi.v1.MasterPageContext + (*HumanProofBanner)(nil), // 16: abi.v1.HumanProofBanner + (*DetailRow)(nil), // 17: abi.v1.DetailRow + (*BlockContext)(nil), // 18: abi.v1.BlockContext + nil, // 19: abi.v1.RenderContext.InjectedSlotsEntry + nil, // 20: abi.v1.RequestInfo.HeadersEntry + nil, // 21: abi.v1.RequestInfo.CookiesEntry + nil, // 22: abi.v1.BlockContext.QueryEntry + nil, // 23: abi.v1.BlockContext.CookiesEntry + nil, // 24: abi.v1.BlockContext.HeadersEntry + (*timestamppb.Timestamp)(nil), // 25: google.protobuf.Timestamp +} +var file_v1_render_proto_depIdxs = []int32{ + 9, // 0: abi.v1.RenderBlockRequest.render_context:type_name -> abi.v1.RenderContext + 2, // 1: abi.v1.RenderBlockResponse.powered:type_name -> abi.v1.PoweredBlock + 9, // 2: abi.v1.RenderTemplateRequest.render_context:type_name -> abi.v1.RenderContext + 9, // 3: abi.v1.RenderTagRequest.render_context:type_name -> abi.v1.RenderContext + 10, // 4: abi.v1.RenderContext.request:type_name -> abi.v1.RequestInfo + 18, // 5: abi.v1.RenderContext.block_context:type_name -> abi.v1.BlockContext + 11, // 6: abi.v1.RenderContext.page:type_name -> abi.v1.PageContext + 12, // 7: abi.v1.RenderContext.post:type_name -> abi.v1.PostContext + 13, // 8: abi.v1.RenderContext.author:type_name -> abi.v1.AuthorContext + 14, // 9: abi.v1.RenderContext.category:type_name -> abi.v1.CategoryContext + 15, // 10: abi.v1.RenderContext.master_page:type_name -> abi.v1.MasterPageContext + 19, // 11: abi.v1.RenderContext.injected_slots:type_name -> abi.v1.RenderContext.InjectedSlotsEntry + 16, // 12: abi.v1.RenderContext.human_proof_banner:type_name -> abi.v1.HumanProofBanner + 17, // 13: abi.v1.RenderContext.detail_row:type_name -> abi.v1.DetailRow + 20, // 14: abi.v1.RequestInfo.headers:type_name -> abi.v1.RequestInfo.HeadersEntry + 21, // 15: abi.v1.RequestInfo.cookies:type_name -> abi.v1.RequestInfo.CookiesEntry + 25, // 16: abi.v1.PostContext.published_at:type_name -> google.protobuf.Timestamp + 25, // 17: abi.v1.BlockContext.now:type_name -> google.protobuf.Timestamp + 22, // 18: abi.v1.BlockContext.query:type_name -> abi.v1.BlockContext.QueryEntry + 23, // 19: abi.v1.BlockContext.cookies:type_name -> abi.v1.BlockContext.CookiesEntry + 24, // 20: abi.v1.BlockContext.headers:type_name -> abi.v1.BlockContext.HeadersEntry + 21, // [21:21] is the sub-list for method output_type + 21, // [21:21] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name +} + +func init() { file_v1_render_proto_init() } +func file_v1_render_proto_init() { + if File_v1_render_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_render_proto_rawDesc), len(file_v1_render_proto_rawDesc)), + NumEnums: 0, + NumMessages: 25, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_v1_render_proto_goTypes, + DependencyIndexes: file_v1_render_proto_depIdxs, + MessageInfos: file_v1_render_proto_msgTypes, + }.Build() + File_v1_render_proto = out.File + file_v1_render_proto_goTypes = nil + file_v1_render_proto_depIdxs = nil +} diff --git a/ai/tools.go b/ai/tools.go new file mode 100644 index 0000000..01da228 --- /dev/null +++ b/ai/tools.go @@ -0,0 +1,28 @@ +package ai + +import "context" + +// ToolResult holds the output of a tool execution. +type ToolResult struct { + Content string `json:"content"` + Error string `json:"error,omitempty"` +} + +// ToolHandler is the function signature for executing a tool. +type ToolHandler func(ctx context.Context, params map[string]any) (*ToolResult, error) + +// ToolDefinition describes a registered tool. +type ToolDefinition struct { + Slug string + Name string + Description string + ParameterSchema map[string]any + Handler ToolHandler +} + +// ToolRegistry is the interface for registering AI tools. +// Plugins use this to register their custom tools. +// The CMS provides the concrete implementation. +type ToolRegistry interface { + Register(tool *ToolDefinition) +} diff --git a/auth/claims.go b/auth/claims.go new file mode 100644 index 0000000..5cf7be8 --- /dev/null +++ b/auth/claims.go @@ -0,0 +1,18 @@ +package auth + +import "github.com/google/uuid" + +// Claims represents admin user JWT claims. +// Plugins receive these from context — they don't parse JWTs themselves. +type Claims struct { + UserID uuid.UUID + Email string + Role string +} + +// PublicClaims represents public/community user JWT claims. +type PublicClaims struct { + UserID uuid.UUID + Email string + Username string +} diff --git a/auth/context.go b/auth/context.go new file mode 100644 index 0000000..62eb9ec --- /dev/null +++ b/auth/context.go @@ -0,0 +1,34 @@ +package auth + +import "context" + +type contextKey string + +const ( + userContextKey contextKey = "user" + publicUserContextKey contextKey = "public_user" +) + +// GetUserFromContext retrieves admin user claims from context. +func GetUserFromContext(ctx context.Context) (*Claims, bool) { + claims, ok := ctx.Value(userContextKey).(*Claims) + return claims, ok +} + +// GetPublicUserFromContext retrieves public user claims from context. +func GetPublicUserFromContext(ctx context.Context) (*PublicClaims, bool) { + claims, ok := ctx.Value(publicUserContextKey).(*PublicClaims) + return claims, ok +} + +// WithUser adds admin user claims to context. +// Used by CMS auth middleware — not typically called by plugins. +func WithUser(ctx context.Context, claims *Claims) context.Context { + return context.WithValue(ctx, userContextKey, claims) +} + +// WithPublicUser adds public user claims to context. +// Used by CMS auth middleware — not typically called by plugins. +func WithPublicUser(ctx context.Context, claims *PublicClaims) context.Context { + return context.WithValue(ctx, publicUserContextKey, claims) +} diff --git a/auth/public_users.go b/auth/public_users.go new file mode 100644 index 0000000..63ae053 --- /dev/null +++ b/auth/public_users.go @@ -0,0 +1,25 @@ +package auth + +import ( + "context" + + "github.com/google/uuid" +) + +// PublicUsers provides public/community user profile access for plugins. +type PublicUsers interface { + GetByUsername(ctx context.Context, username string) (*PublicUserProfile, error) + GetByID(ctx context.Context, id uuid.UUID) (*PublicUserProfile, error) +} + +// PublicUserProfile is a plugin-facing view of a public user. +type PublicUserProfile struct { + ID uuid.UUID + Email string + Username string + DisplayName string + AvatarURL string + Bio string + EmailVerified bool + Role string +} diff --git a/auth/trustedheaders.go b/auth/trustedheaders.go new file mode 100644 index 0000000..5ce34b6 --- /dev/null +++ b/auth/trustedheaders.go @@ -0,0 +1,91 @@ +package auth + +import ( + "context" + "net/http" + + "github.com/google/uuid" +) + +// Trusted identity headers — the wasm plugin auth contract. +// +// Context values do NOT cross the wasm ABI, so a guest cannot see the auth +// context the host's middleware built. The SECURE way to convey the caller's +// identity to a guest is these headers, populated HOST-SIDE from the +// signature-verified principal after the host has run its RBAC guard +// (rbac.Authorize against the verified JWT). The host STRIPS any client-supplied +// copy of these headers before setting them, so a guest may trust them +// unconditionally. +// +// A guest MUST NOT decode a client-supplied cookie or Authorization/Bearer +// token to establish identity — those are attacker-controlled across the +// boundary (the host forwards the raw request), and trusting them is a +// privilege-escalation bug. Read identity ONLY from these headers (via +// TrustedHeaderMiddleware / ContextFromTrustedHeaders). See +// core/docs/wasm-abi.md ("Trusted identity headers"). +// +// Values are the canonical MIME header form so a host map-set and a guest +// http.Header.Get agree without re-canonicalization surprises. +const ( + // Admin principal (auth.Claims). + HeaderVerifiedUserID = "X-Bn-Verified-User-Id" + HeaderVerifiedRole = "X-Bn-Verified-Role" + HeaderVerifiedEmail = "X-Bn-Verified-Email" + + // Public/community principal (auth.PublicClaims). + HeaderVerifiedPublicUserID = "X-Bn-Verified-Public-User-Id" + HeaderVerifiedPublicUsername = "X-Bn-Verified-Public-Username" + HeaderVerifiedPublicEmail = "X-Bn-Verified-Public-Email" +) + +// AllTrustedHeaders lists every trusted identity header, canonical form. The +// host deletes each of these from the inbound request before injecting its own +// verified values, so a client cannot forge one. +func AllTrustedHeaders() []string { + return []string{ + HeaderVerifiedUserID, + HeaderVerifiedRole, + HeaderVerifiedEmail, + HeaderVerifiedPublicUserID, + HeaderVerifiedPublicUsername, + HeaderVerifiedPublicEmail, + } +} + +// ContextFromTrustedHeaders rebuilds the request's auth context from the host's +// verified identity headers. Trust model: see the const block above — these +// headers are host-controlled, so no token parsing or signature check happens +// here (the guest has no signing secret by design). A malformed user-id header +// is ignored rather than trusted. +func ContextFromTrustedHeaders(ctx context.Context, h http.Header) context.Context { + if s := h.Get(HeaderVerifiedUserID); s != "" { + if id, err := uuid.Parse(s); err == nil { + ctx = WithUser(ctx, &Claims{ + UserID: id, + Email: h.Get(HeaderVerifiedEmail), + Role: h.Get(HeaderVerifiedRole), + }) + } + } + if s := h.Get(HeaderVerifiedPublicUserID); s != "" { + if id, err := uuid.Parse(s); err == nil { + ctx = WithPublicUser(ctx, &PublicClaims{ + UserID: id, + Email: h.Get(HeaderVerifiedPublicEmail), + Username: h.Get(HeaderVerifiedPublicUsername), + }) + } + } + return ctx +} + +// TrustedHeaderMiddleware wraps a guest HTTP handler so downstream RPCs can read +// auth.GetUserFromContext / GetPublicUserFromContext. Plugins whose Connect RPCs +// resolve the caller from context MUST mount this around their handler (context +// does not cross the ABI otherwise). This is the ONLY sanctioned way for a guest +// to learn the caller's identity. +func TrustedHeaderMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r.WithContext(ContextFromTrustedHeaders(r.Context(), r.Header))) + }) +} diff --git a/auth/trustedheaders_test.go b/auth/trustedheaders_test.go new file mode 100644 index 0000000..27a09c5 --- /dev/null +++ b/auth/trustedheaders_test.go @@ -0,0 +1,82 @@ +package auth + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" +) + +func TestContextFromTrustedHeaders(t *testing.T) { + adminID := uuid.New() + pubID := uuid.New() + + h := http.Header{} + h.Set(HeaderVerifiedUserID, adminID.String()) + h.Set(HeaderVerifiedRole, "superadmin") + h.Set(HeaderVerifiedEmail, "admin@example.com") + h.Set(HeaderVerifiedPublicUserID, pubID.String()) + h.Set(HeaderVerifiedPublicUsername, "ninja") + h.Set(HeaderVerifiedPublicEmail, "ninja@example.com") + + ctx := ContextFromTrustedHeaders(context.Background(), h) + + c, ok := GetUserFromContext(ctx) + if !ok { + t.Fatal("admin claims missing") + } + if c.UserID != adminID || c.Role != "superadmin" || c.Email != "admin@example.com" { + t.Fatalf("admin claims mismatch: %+v", c) + } + p, ok := GetPublicUserFromContext(ctx) + if !ok { + t.Fatal("public claims missing") + } + if p.UserID != pubID || p.Username != "ninja" || p.Email != "ninja@example.com" { + t.Fatalf("public claims mismatch: %+v", p) + } +} + +// TestContextFromTrustedHeaders_NoneWhenAbsent proves an anonymous request (no +// trusted headers) yields no principals — nothing is fabricated. +func TestContextFromTrustedHeaders_NoneWhenAbsent(t *testing.T) { + ctx := ContextFromTrustedHeaders(context.Background(), http.Header{}) + if _, ok := GetUserFromContext(ctx); ok { + t.Fatal("unexpected admin claims for anonymous request") + } + if _, ok := GetPublicUserFromContext(ctx); ok { + t.Fatal("unexpected public claims for anonymous request") + } +} + +// TestContextFromTrustedHeaders_BadUUIDIgnored proves a malformed id header is +// ignored rather than trusted (no panic, no principal). +func TestContextFromTrustedHeaders_BadUUIDIgnored(t *testing.T) { + h := http.Header{} + h.Set(HeaderVerifiedUserID, "not-a-uuid") + h.Set(HeaderVerifiedRole, "superadmin") + ctx := ContextFromTrustedHeaders(context.Background(), h) + if _, ok := GetUserFromContext(ctx); ok { + t.Fatal("malformed user-id header must not yield a principal") + } +} + +// TestTrustedHeaderMiddleware proves the middleware installs the principal into +// the downstream handler's request context, keyed by the canonical header form. +func TestTrustedHeaderMiddleware(t *testing.T) { + adminID := uuid.New() + var got *Claims + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got, _ = GetUserFromContext(r.Context()) + }) + req := httptest.NewRequest(http.MethodGet, "/x", nil) + // lower-case on the way in — net/http canonicalizes, middleware must still see it. + req.Header.Set("x-bn-verified-user-id", adminID.String()) + req.Header.Set("x-bn-verified-role", "admin") + TrustedHeaderMiddleware(next).ServeHTTP(httptest.NewRecorder(), req) + if got == nil || got.UserID != adminID || got.Role != "admin" { + t.Fatalf("middleware did not install principal: %+v", got) + } +} diff --git a/blocks/builtin/html.go b/blocks/builtin/html.go new file mode 100644 index 0000000..4002783 --- /dev/null +++ b/blocks/builtin/html.go @@ -0,0 +1,299 @@ +package builtin + +import ( + "context" + "fmt" + "html" + "maps" + "regexp" + "strings" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "github.com/google/uuid" +) + +var HTMLBlockMeta = blocks.BlockMeta{ + Key: "html", + Title: "HTML", + Description: "Raw HTML content with template variable support", + Category: blocks.CategoryContent, +} + +var mediaURLRegex = regexp.MustCompile(`(src|href)=["']media:([^"']+)["']`) + +func HTMLBlock(ctx context.Context, content map[string]any) string { + var htmlTemplate string + if htmlContent, ok := content["_html_content"].(string); ok && htmlContent != "" { + htmlTemplate = htmlContent + } else { + htmlTemplate, _ = content["html"].(string) + } + + if htmlTemplate == "" { + return "" + } + + enrichedContent := enrichMediaFields(ctx, content) + + result, err := blocks.RenderTemplate(htmlTemplate, enrichedContent) + if err != nil { + if blocks.IsEditor(ctx) { + return fmt.Sprintf(`
+ Template Error: %s +
`, html.EscapeString(err.Error())) + } + return "" + } + + result = blocks.ProcessIcons(result) + result = ResolveMediaURLs(result) + + return result +} + +func enrichMediaFields(ctx context.Context, content map[string]any) map[string]any { + mediaFields := GetMediaFields(content) + if len(mediaFields) == 0 { + return content + } + + resolver := blocks.GetMediaResolver(ctx) + + enriched := make(map[string]any, len(content)) + maps.Copy(enriched, content) + + for fieldName := range mediaFields { + value, ok := content[fieldName].(string) + if !ok || value == "" { + continue + } + + mediaValue := parseMediaValue(ctx, resolver, value) + if mediaValue != nil { + enriched[fieldName] = mediaValue + } + } + + return enriched +} + +func parseMediaValue(ctx context.Context, resolver blocks.MediaResolver, value string) *blocks.MediaValue { + if value == "" { + return nil + } + + path := value + if after, ok := strings.CutPrefix(value, "media:"); ok { + path = after + } + + src := path + if !strings.HasPrefix(path, "/media/") { + src = "/media/" + path + } + + parts := strings.SplitN(path, "/", 2) + if len(parts) == 0 || parts[0] == "" { + return &blocks.MediaValue{Src: src} + } + + mediaID, err := uuid.Parse(parts[0]) + if err != nil { + return &blocks.MediaValue{Src: src} + } + + if resolver == nil { + return &blocks.MediaValue{Src: src} + } + + resolved := resolver.ResolveMedia(ctx, mediaID) + if resolved == nil { + return &blocks.MediaValue{Src: src} + } + + resolved.Src = src + return resolved +} + +// GetMediaFields extracts field names that have x-editor: "media" from customSchema. +func GetMediaFields(content map[string]any) map[string]bool { + result := make(map[string]bool) + + customSchema, ok := content["customSchema"].(map[string]any) + if !ok { + return result + } + + properties, ok := customSchema["properties"].(map[string]any) + if !ok { + return result + } + + for key, prop := range properties { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + if editor, ok := propMap["x-editor"].(string); ok && editor == "media" { + result[key] = true + } + } + + return result +} + +// ResolveMediaURLs converts media: prefixed URLs to proper /media/ paths. +func ResolveMediaURLs(htmlStr string) string { + return mediaURLRegex.ReplaceAllStringFunc(htmlStr, func(match string) string { + submatches := mediaURLRegex.FindStringSubmatch(match) + if len(submatches) < 3 { + return match + } + attr := submatches[1] + path := submatches[2] + + if strings.HasPrefix(path, "/media/") { + return fmt.Sprintf(`%s="%s"`, attr, path) + } + return fmt.Sprintf(`%s="/media/%s"`, attr, path) + }) +} + +var ( + boldRegex = regexp.MustCompile(`\*\*(.+?)\*\*`) + italicRegex = regexp.MustCompile(`\*([^*]+?)\*`) + colorRegex = regexp.MustCompile(`==(?:([a-z-]+):)?(.+?)==`) + linkRegex = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) +) + +var validThemeColors = map[string]bool{ + "foreground": true, "background": true, + "primary": true, "primary-foreground": true, + "secondary": true, "secondary-foreground": true, + "muted": true, "muted-foreground": true, + "accent": true, "accent-foreground": true, + "destructive": true, "destructive-foreground": true, + "border": true, "card": true, "card-foreground": true, +} + +// ProcessMarkdown converts markdown-like syntax to HTML. +func ProcessMarkdown(text string) string { + text = boldRegex.ReplaceAllString(text, `$1`) + text = italicRegex.ReplaceAllString(text, `$1`) + text = colorRegex.ReplaceAllStringFunc(text, func(match string) string { + submatches := colorRegex.FindStringSubmatch(match) + if len(submatches) < 3 { + return match + } + colorName := submatches[1] + content := submatches[2] + + if colorName == "" || !validThemeColors[colorName] { + colorName = "accent" + } + + return fmt.Sprintf(`%s`, colorName, content) + }) + text = linkRegex.ReplaceAllStringFunc(text, func(match string) string { + submatches := linkRegex.FindStringSubmatch(match) + if len(submatches) < 3 { + return match + } + linkText := submatches[1] + url := submatches[2] + url = strings.ReplaceAll(url, "&", "&") + url = strings.ReplaceAll(url, """, "\"") + url = strings.ReplaceAll(url, "'", "'") + return fmt.Sprintf(`%s`, html.EscapeString(url), linkText) + }) + return text +} + +// WrapLinesInParagraphs converts textarea newlines to
tags. +func WrapLinesInParagraphs(text string) string { + return strings.ReplaceAll(text, "\n", "
") +} + +// GetTextareaFields extracts field names that have x-editor: textarea from customSchema. +func GetTextareaFields(content map[string]any) map[string]bool { + result := make(map[string]bool) + + customSchema, ok := content["customSchema"].(map[string]any) + if !ok { + return result + } + + properties, ok := customSchema["properties"].(map[string]any) + if !ok { + return result + } + + for key, prop := range properties { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" { + result[key] = true + } + } + + return result +} + +// GetArrayItemTextareaFields extracts textarea field names from an array's item schema. +func GetArrayItemTextareaFields(content map[string]any, collectionName string) map[string]bool { + result := make(map[string]bool) + + customSchema, ok := content["customSchema"].(map[string]any) + if !ok { + return result + } + + properties, ok := customSchema["properties"].(map[string]any) + if !ok { + return result + } + + arrayField, ok := properties[collectionName].(map[string]any) + if !ok { + return result + } + + if itemProperties, ok := arrayField["itemProperties"].(map[string]any); ok { + for _, prop := range itemProperties { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" { + if title, ok := propMap["title"].(string); ok && title != "" { + result[title] = true + } + } + } + return result + } + + items, ok := arrayField["items"].(map[string]any) + if !ok { + return result + } + + itemProps, ok := items["properties"].(map[string]any) + if !ok { + return result + } + + for key, prop := range itemProps { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + if editor, ok := propMap["x-editor"].(string); ok && editor == "textarea" { + result[key] = true + } + } + + return result +} diff --git a/blocks/context.go b/blocks/context.go new file mode 100644 index 0000000..1922ea5 --- /dev/null +++ b/blocks/context.go @@ -0,0 +1,469 @@ +package blocks + +import ( + "context" + "net/http" + "time" + + "github.com/google/uuid" +) + +// --- Typed context structs --- + +// PageContext provides page information to blocks via context. +// The CMS populates this from its internal db.Page type. +type PageContext struct { + ID uuid.UUID + Slug string + Title string + PostType string // "page", "post", "master", "system" + Status string // "published", "draft", "scheduled" +} + +// PostContext provides blog post information to blocks via context. +type PostContext struct { + ID uuid.UUID + Slug string + Title string + Excerpt string + FeaturedImageURL string + AuthorID uuid.UUID + PublishedAt time.Time + ReadingTime int + IsFeatured bool +} + +// AuthorContext provides author information to blocks via context. +type AuthorContext struct { + ID uuid.UUID + Name string + Slug string + Bio string + AvatarURL string +} + +// CategoryContext provides category information to blocks via context. +type CategoryContext struct { + ID uuid.UUID + Name string + Slug string +} + +// MasterPageContext provides master page information during rendering. +type MasterPageContext struct { + ID uuid.UUID + Slug string + Title string +} + +// EmbedResolver resolves embedded component blocks within blog content. +type EmbedResolver interface { + RenderEmbed(ctx context.Context, blockID uuid.UUID, dataSource, layout string) string +} + +// --- Template key --- + +type templateKeyContextKey struct{} + +func WithTemplateKey(ctx context.Context, templateKey string) context.Context { + return context.WithValue(ctx, templateKeyContextKey{}, templateKey) +} + +func GetTemplateKey(ctx context.Context) string { + if v, ok := ctx.Value(templateKeyContextKey{}).(string); ok { + return v + } + return "" +} + +// --- Queries (generic) --- + +type queriesContextKey struct{} + +// WithQueries stores a queries value in context. Use with GetQueries[T]. +func WithQueries[T any](ctx context.Context, queries T) context.Context { + return context.WithValue(ctx, queriesContextKey{}, queries) +} + +// GetQueries retrieves a typed queries value from context. +// Usage: queries, ok := blocks.GetQueries[*db.Queries](ctx) +func GetQueries[T any](ctx context.Context) (T, bool) { + val, ok := ctx.Value(queriesContextKey{}).(T) + return val, ok +} + +// --- Current page --- + +type currentPageContextKey struct{} + +func WithCurrentPage(ctx context.Context, page *PageContext) context.Context { + return context.WithValue(ctx, currentPageContextKey{}, page) +} + +func GetCurrentPage(ctx context.Context) *PageContext { + if v, ok := ctx.Value(currentPageContextKey{}).(*PageContext); ok { + return v + } + return nil +} + +// --- Current blog post --- + +type currentBlogPostContextKey struct{} + +func WithCurrentBlogPost(ctx context.Context, post *PostContext) context.Context { + return context.WithValue(ctx, currentBlogPostContextKey{}, post) +} + +func GetCurrentBlogPost(ctx context.Context) *PostContext { + if v, ok := ctx.Value(currentBlogPostContextKey{}).(*PostContext); ok { + return v + } + return nil +} + +// --- Current author --- + +type currentAuthorContextKey struct{} + +func WithCurrentAuthor(ctx context.Context, author *AuthorContext) context.Context { + return context.WithValue(ctx, currentAuthorContextKey{}, author) +} + +func GetCurrentAuthor(ctx context.Context) *AuthorContext { + if v, ok := ctx.Value(currentAuthorContextKey{}).(*AuthorContext); ok { + return v + } + return nil +} + +// --- Current category --- + +type currentCategoryContextKey struct{} + +func WithCurrentCategory(ctx context.Context, category *CategoryContext) context.Context { + return context.WithValue(ctx, currentCategoryContextKey{}, category) +} + +func GetCurrentCategory(ctx context.Context) *CategoryContext { + if v, ok := ctx.Value(currentCategoryContextKey{}).(*CategoryContext); ok { + return v + } + return nil +} + +// --- Requested path (for 404 pages) --- + +type requestedPathContextKey struct{} + +func WithRequestedPath(ctx context.Context, path string) context.Context { + return context.WithValue(ctx, requestedPathContextKey{}, path) +} + +func GetRequestedPath(ctx context.Context) string { + if v, ok := ctx.Value(requestedPathContextKey{}).(string); ok { + return v + } + return "" +} + +// --- Injected slots (master page rendering) --- + +type injectedSlotsContextKey struct{} + +func WithInjectedSlots(ctx context.Context, slots map[string]string) context.Context { + return context.WithValue(ctx, injectedSlotsContextKey{}, slots) +} + +func GetInjectedSlotContent(ctx context.Context, slotName string) string { + if slots, ok := ctx.Value(injectedSlotsContextKey{}).(map[string]string); ok { + return slots[slotName] + } + return "" +} + +func GetInjectedSlots(ctx context.Context) map[string]string { + if slots, ok := ctx.Value(injectedSlotsContextKey{}).(map[string]string); ok { + return slots + } + return nil +} + +// --- Master page --- + +type masterPageContextKey struct{} + +func WithMasterPage(ctx context.Context, masterPage *MasterPageContext) context.Context { + return context.WithValue(ctx, masterPageContextKey{}, masterPage) +} + +func GetMasterPage(ctx context.Context) *MasterPageContext { + if v, ok := ctx.Value(masterPageContextKey{}).(*MasterPageContext); ok { + return v + } + return nil +} + +func IsMasterPageContext(ctx context.Context) bool { + return ctx.Value(masterPageContextKey{}) != nil +} + +// --- HTTP request --- + +type requestContextKey struct{} + +func WithRequest(ctx context.Context, r *http.Request) context.Context { + return context.WithValue(ctx, requestContextKey{}, r) +} + +func GetRequest(ctx context.Context) *http.Request { + if r, ok := ctx.Value(requestContextKey{}).(*http.Request); ok { + return r + } + return nil +} + +// --- Editor mode --- + +type isEditorContextKey struct{} + +func WithIsEditor(ctx context.Context, isEditor bool) context.Context { + return context.WithValue(ctx, isEditorContextKey{}, isEditor) +} + +func IsEditor(ctx context.Context) bool { + if v, ok := ctx.Value(isEditorContextKey{}).(bool); ok { + return v + } + return false +} + +// --- Expected slots --- + +type expectedSlotsContextKey struct{} + +func WithExpectedSlots(ctx context.Context, slots []string) context.Context { + return context.WithValue(ctx, expectedSlotsContextKey{}, slots) +} + +func GetExpectedSlots(ctx context.Context) []string { + if v, ok := ctx.Value(expectedSlotsContextKey{}).([]string); ok { + return v + } + return nil +} + +// --- Block ID --- + +type blockIDContextKey struct{} + +func WithBlockID(ctx context.Context, id uuid.UUID) context.Context { + return context.WithValue(ctx, blockIDContextKey{}, id) +} + +func GetBlockID(ctx context.Context) uuid.UUID { + if v, ok := ctx.Value(blockIDContextKey{}).(uuid.UUID); ok { + return v + } + return uuid.Nil +} + +// --- Current page ID --- + +type currentPageIDContextKey struct{} + +func WithCurrentPageID(ctx context.Context, id uuid.UUID) context.Context { + return context.WithValue(ctx, currentPageIDContextKey{}, id) +} + +func GetCurrentPageID(ctx context.Context) uuid.UUID { + if v, ok := ctx.Value(currentPageIDContextKey{}).(uuid.UUID); ok { + return v + } + return uuid.Nil +} + +// --- Slot renderer --- + +type slotRendererContextKey struct{} + +func WithSlotRenderer(ctx context.Context, r SlotRenderer) context.Context { + return context.WithValue(ctx, slotRendererContextKey{}, r) +} + +func GetSlotRenderer(ctx context.Context) SlotRenderer { + if r, ok := ctx.Value(slotRendererContextKey{}).(SlotRenderer); ok { + return r + } + return nil +} + +// --- Human proof banner --- + +type humanProofBannerContextKey struct{} + +func WithHumanProofBanner(ctx context.Context, data *HumanProofBannerData) context.Context { + return context.WithValue(ctx, humanProofBannerContextKey{}, data) +} + +func GetHumanProofBanner(ctx context.Context) *HumanProofBannerData { + if v, ok := ctx.Value(humanProofBannerContextKey{}).(*HumanProofBannerData); ok { + return v + } + return nil +} + +// --- Media resolver --- + +// MediaResolver resolves media metadata from storage. +// The CMS implements this with db.Queries.GetMedia. +type MediaResolver interface { + ResolveMedia(ctx context.Context, mediaID uuid.UUID) *MediaValue +} + +type mediaResolverContextKey struct{} + +func WithMediaResolver(ctx context.Context, resolver MediaResolver) context.Context { + return context.WithValue(ctx, mediaResolverContextKey{}, resolver) +} + +func GetMediaResolver(ctx context.Context) MediaResolver { + if r, ok := ctx.Value(mediaResolverContextKey{}).(MediaResolver); ok { + return r + } + return nil +} + +// --- Embed resolver --- + +type embedResolverContextKey struct{} + +func WithEmbedResolver(ctx context.Context, resolver EmbedResolver) context.Context { + return context.WithValue(ctx, embedResolverContextKey{}, resolver) +} + +func GetEmbedResolver(ctx context.Context) EmbedResolver { + if r, ok := ctx.Value(embedResolverContextKey{}).(EmbedResolver); ok { + return r + } + return nil +} + +// --- RenderSlot --- + +// RenderSlot renders children for the current container block. +func RenderSlot(ctx context.Context) string { + blockID := GetBlockID(ctx) + if blockID == uuid.Nil { + return "" + } + renderer := GetSlotRenderer(ctx) + if renderer == nil { + return "" + } + return renderer.RenderContainerSlot(ctx, blockID) +} + +// --- BlockContext (pongo2 template data) --- + +// BlockContext provides contextual information available to all block templates. +type BlockContext struct { + URL string `pongo2:"url"` + Path string `pongo2:"path"` + Slug string `pongo2:"slug"` + PageID string `pongo2:"pageId"` + PageTitle string `pongo2:"pageTitle"` + TemplateKey string `pongo2:"templateKey"` + IsEditor bool `pongo2:"isEditor"` + Timestamp int64 `pongo2:"timestamp"` + IsLoggedIn bool `pongo2:"isLoggedIn"` + UserID string `pongo2:"userId"` + UserEmail string `pongo2:"userEmail"` + UserRole string `pongo2:"userRole"` + Method string `pongo2:"method"` + Host string `pongo2:"host"` + Query map[string]string `pongo2:"query"` + Referrer string `pongo2:"referrer"` + UserAgent string `pongo2:"userAgent"` + IP string `pongo2:"ip"` + Cookies map[string]string `pongo2:"cookies"` + Headers map[string]string `pongo2:"headers"` + Country string `pongo2:"country"` + City string `pongo2:"city"` + Timezone string `pongo2:"timezone"` + Now time.Time `pongo2:"now"` + CurrentAuthor map[string]any `pongo2:"currentAuthor"` + CurrentPost map[string]any `pongo2:"currentPost"` + CurrentCategory map[string]any `pongo2:"currentCategory"` + Site map[string]any `pongo2:"site"` + IsPublicLoggedIn bool `pongo2:"isPublicLoggedIn"` + PublicUserID string `pongo2:"publicUserId"` + PublicUsername string `pongo2:"publicUsername"` + PublicDisplayName string `pongo2:"publicDisplayName"` + PublicEmailVerified bool `pongo2:"publicEmailVerified"` + DetailRowID string `pongo2:"detailRowId"` + DetailTableID string `pongo2:"detailTableId"` + DetailRowData map[string]any `pongo2:"detailRowData"` + BlogIndexURL string `pongo2:"blogIndexUrl"` + CategoryPageURL string `pongo2:"categoryPageUrl"` +} + +type blockContextKey struct{} + +func WithBlockContext(ctx context.Context, blockCtx *BlockContext) context.Context { + return context.WithValue(ctx, blockContextKey{}, blockCtx) +} + +func GetBlockContext(ctx context.Context) *BlockContext { + if bc, ok := ctx.Value(blockContextKey{}).(*BlockContext); ok { + return bc + } + return nil +} + +// ToMap converts the BlockContext to a map for pongo2 template injection. +func (bc *BlockContext) ToMap() map[string]any { + return map[string]any{ + "url": bc.URL, "path": bc.Path, "slug": bc.Slug, + "pageId": bc.PageID, "pageTitle": bc.PageTitle, + "templateKey": bc.TemplateKey, "isEditor": bc.IsEditor, + "timestamp": bc.Timestamp, "now": bc.Now, + "isLoggedIn": bc.IsLoggedIn, "userId": bc.UserID, + "userEmail": bc.UserEmail, "userRole": bc.UserRole, + "method": bc.Method, "host": bc.Host, "query": bc.Query, + "referrer": bc.Referrer, "userAgent": bc.UserAgent, + "ip": bc.IP, "cookies": bc.Cookies, "headers": bc.Headers, + "country": bc.Country, "city": bc.City, "timezone": bc.Timezone, + "currentAuthor": bc.CurrentAuthor, "currentPost": bc.CurrentPost, + "currentCategory": bc.CurrentCategory, "site": bc.Site, + "isPublicLoggedIn": bc.IsPublicLoggedIn, + "publicUserId": bc.PublicUserID, "publicUsername": bc.PublicUsername, + "publicDisplayName": bc.PublicDisplayName, + "publicEmailVerified": bc.PublicEmailVerified, + "detailRowId": bc.DetailRowID, "detailTableId": bc.DetailTableID, + "detailRowData": bc.DetailRowData, + "blogIndexUrl": bc.BlogIndexURL, "categoryPageUrl": bc.CategoryPageURL, + } +} + +// --- Detail row --- + +// DetailRowInfo holds the resolved data table row for detail pages. +type DetailRowInfo struct { + TableID string + RowID string + Data map[string]any +} + +type detailRowKey struct{} + +func WithDetailRow(ctx context.Context, tableID, rowID string, data map[string]any) context.Context { + return context.WithValue(ctx, detailRowKey{}, &DetailRowInfo{TableID: tableID, RowID: rowID, Data: data}) +} + +func GetDetailRow(ctx context.Context) *DetailRowInfo { + if v, ok := ctx.Value(detailRowKey{}).(*DetailRowInfo); ok { + return v + } + return nil +} diff --git a/blocks/powered.go b/blocks/powered.go new file mode 100644 index 0000000..ff3bd4f --- /dev/null +++ b/blocks/powered.go @@ -0,0 +1,62 @@ +package blocks + +import ( + "encoding/json" + "strings" +) + +// poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL +// bytes so it can never collide with real block HTML: a BlockFunc returns a +// plain HTML string today, and this marker is a distinct, out-of-band signal +// that the block is "powered" (template + data, rendered host-side). +const poweredBlockSentinel = "\x00bn:powered\x00" + +// PoweredResult is the decoded payload of a PoweredBlock marker: the template +// source and its data map. The wasm guest's RENDER_BLOCK handler decodes it +// and forwards it as abiv1.PoweredBlock so the HOST renders the template with +// pongo2 — after the block-invoke has returned, keeping the guest free. +type PoweredResult struct { + Template string `json:"template"` + Data map[string]any `json:"data"` +} + +// PoweredBlock marks a block's return value as "powered": instead of final +// HTML, the block hands back a template string plus a data map, and the host +// renders it (pongo2/ninjatpl) host-side. Return its result directly from a +// BlockFunc: +// +// func MyBlock(ctx context.Context, content map[string]any) string { +// posts := loadPosts(ctx) // build data via capabilities +// return blocks.PoweredBlock(tmpl, map[string]any{"posts": posts}) +// } +// +// This is the guest-safe replacement for calling blocks.RenderTemplate inside +// a block: pongo2 never crosses the wasm boundary, so the guest cannot render +// itself — it defers rendering to the host. Because the host renders only +// after RENDER_BLOCK returns, any plugin-declared tag/filter the template +// hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke, +// never a re-entrant one. +func PoweredBlock(template string, data map[string]any) string { + payload, err := json.Marshal(PoweredResult{Template: template, Data: data}) + if err != nil { + // A non-serializable data map is a programming error; fall back to an + // empty-data powered result so the template still renders. + payload, _ = json.Marshal(PoweredResult{Template: template}) + } + return poweredBlockSentinel + string(payload) +} + +// DecodePoweredBlock reports whether s is a PoweredBlock marker and, if so, +// returns the decoded template + data. The wasm guest uses it to distinguish a +// powered result from plain HTML. A non-marker (ordinary HTML) returns ok=false. +func DecodePoweredBlock(s string) (PoweredResult, bool) { + rest, ok := strings.CutPrefix(s, poweredBlockSentinel) + if !ok { + return PoweredResult{}, false + } + var pr PoweredResult + if err := json.Unmarshal([]byte(rest), &pr); err != nil { + return PoweredResult{}, false + } + return pr, true +} diff --git a/blocks/powered_test.go b/blocks/powered_test.go new file mode 100644 index 0000000..2095c02 --- /dev/null +++ b/blocks/powered_test.go @@ -0,0 +1,59 @@ +package blocks + +import "testing" + +func TestPoweredBlockRoundTrip(t *testing.T) { + marker := PoweredBlock("

{{ x }}

", map[string]any{"x": "y"}) + pr, ok := DecodePoweredBlock(marker) + if !ok { + t.Fatal("DecodePoweredBlock did not recognize its own marker") + } + if pr.Template != "

{{ x }}

" { + t.Errorf("template = %q", pr.Template) + } + if pr.Data["x"] != "y" { + t.Errorf("data = %v", pr.Data) + } +} + +func TestDecodePoweredBlockRejectsPlainHTML(t *testing.T) { + if _, ok := DecodePoweredBlock("

hello

"); ok { + t.Error("plain HTML must not decode as a powered block") + } + if _, ok := DecodePoweredBlock(""); ok { + t.Error("empty string must not decode as a powered block") + } +} + +func TestTagFilterRegistry(t *testing.T) { + ResetRegisteredTagsAndFilters() + t.Cleanup(ResetRegisteredTagsAndFilters) + + RegisterTag("b", func(map[string]any, RenderContext) (string, error) { return "b", nil }) + RegisterTag("a", func(map[string]any, RenderContext) (string, error) { return "a", nil }) + RegisterFilter("f", func(string, map[string]any) (string, error) { return "f", nil }) + // Empty name / nil fn are ignored. + RegisterTag("", nil) + RegisterFilter("g", nil) + + if names := RegisteredTagNames(); len(names) != 2 || names[0] != "a" || names[1] != "b" { + t.Errorf("tag names = %v, want sorted [a b]", names) + } + if names := RegisteredFilterNames(); len(names) != 1 || names[0] != "f" { + t.Errorf("filter names = %v, want [f]", names) + } + if _, ok := LookupTag("a"); !ok { + t.Error("LookupTag(a) missing") + } + if _, ok := LookupFilter("f"); !ok { + t.Error("LookupFilter(f) missing") + } + if _, ok := LookupTag("missing"); ok { + t.Error("LookupTag(missing) should be absent") + } + + ResetRegisteredTagsAndFilters() + if RegisteredTagNames() != nil || RegisteredFilterNames() != nil { + t.Error("reset did not clear registries") + } +} diff --git a/blocks/registry.go b/blocks/registry.go new file mode 100644 index 0000000..cd1e3ef --- /dev/null +++ b/blocks/registry.go @@ -0,0 +1,12 @@ +package blocks + +import "io/fs" + +// BlockRegistry is the interface that plugins use to register blocks. +type BlockRegistry interface { + Register(meta BlockMeta, fn BlockFunc) + RegisterTemplateOverride(templateKey, blockKey string, fn BlockFunc) + RegisterTemplateOverrideWithSource(templateKey, blockKey, source string, fn BlockFunc) + LoadSchemasFromFS(fsys fs.FS) error + LoadSchemasFromFSWithPrefix(fsys fs.FS, prefix string) error +} diff --git a/blocks/shared/button.go b/blocks/shared/button.go new file mode 100644 index 0000000..0b0cffc --- /dev/null +++ b/blocks/shared/button.go @@ -0,0 +1,250 @@ +package shared + +import ( + "strconv" + "strings" +) + +// ButtonConfig represents a unified button configuration +type ButtonConfig struct { + // Content + Text string + URL string + + // Type determines theme defaults + // Built-in: "primary" | "secondary" | "outline" | "ghost" | "link" | "destructive" + // Custom: any user-defined type name from theme.buttons.custom + Type string + + // Override Mode + UseThemeDefaults bool // When true, uses CSS variables from theme + + // Custom Overrides (only used when UseThemeDefaults = false) + TextColor string // Tailwind class like "text-white" or "text-[#ff5500]" + TextOpacity int // 0-100 + BgColor string // Tailwind class like "bg-primary" or "bg-[#ff5500]" + BgOpacity int // 0-100 + + // Effects (can override theme) + Pill bool // Fully rounded corners + Elevated bool // Shadow + ThreeDee bool // 3D push effect + Size string // "sm" | "md" | "lg" + + // Behavior + OpenNewTab bool +} + +// Classes returns CSS classes for the button +func (b ButtonConfig) Classes() string { + var classes []string + classes = append(classes, "btn") + + // Size class + switch b.Size { + case "sm": + classes = append(classes, "btn-sm") + case "lg": + classes = append(classes, "btn-lg") + default: + classes = append(classes, "btn-md") + } + + if b.UseThemeDefaults { + // Use theme CSS variable classes + btnType := b.Type + if btnType == "" { + btnType = "primary" + } + classes = append(classes, "btn-"+btnType) + } else { + // Use custom Tailwind classes + if b.BgColor != "" { + classes = append(classes, b.BgColor) + } else { + // Default based on type variant + switch b.Type { + case "outline": + classes = append(classes, "border-2", "border-primary", "bg-transparent") + case "ghost": + classes = append(classes, "bg-transparent", "hover:bg-accent") + case "link": + classes = append(classes, "bg-transparent", "underline", "underline-offset-4") + case "destructive": + classes = append(classes, "bg-destructive", "hover:bg-destructive/90") + default: // primary, secondary, solid + classes = append(classes, "bg-primary", "hover:bg-primary/90") + } + } + + if b.TextColor != "" { + classes = append(classes, b.TextColor) + } else { + switch b.Type { + case "outline": + classes = append(classes, "text-primary") + case "ghost": + classes = append(classes, "text-foreground", "hover:text-accent-foreground") + case "link": + classes = append(classes, "text-primary") + case "destructive": + classes = append(classes, "text-destructive-foreground") + default: + classes = append(classes, "text-primary-foreground") + } + } + } + + // Effects that can override theme + if b.Pill { + classes = append(classes, "rounded-full") + } + + if b.Elevated && b.Type != "ghost" && b.Type != "link" { + classes = append(classes, "shadow-lg", "hover:shadow-xl") + } + + if b.ThreeDee && (b.Type == "" || b.Type == "primary" || b.Type == "solid") { + classes = append(classes, "border-b-4", "border-primary/70", "active:border-b-2", "active:mt-0.5") + } + + return strings.Join(classes, " ") +} + +// InlineStyle returns inline CSS styles for opacity if needed +func (b ButtonConfig) InlineStyle() string { + var styles []string + + if !b.UseThemeDefaults { + if b.TextOpacity > 0 && b.TextOpacity < 100 { + opacity := float64(b.TextOpacity) / 100.0 + styles = append(styles, "color-opacity: "+strconv.FormatFloat(opacity, 'f', 2, 64)) + } + if b.BgOpacity > 0 && b.BgOpacity < 100 { + opacity := float64(b.BgOpacity) / 100.0 + styles = append(styles, "--tw-bg-opacity: "+strconv.FormatFloat(opacity, 'f', 2, 64)) + } + } + + if len(styles) == 0 { + return "" + } + return strings.Join(styles, "; ") +} + +// ParseButton extracts ButtonConfig from JSON content +func ParseButton(value any, defaultType string) ButtonConfig { + btn := ButtonConfig{ + Type: defaultType, + UseThemeDefaults: true, // Default to theme styling + TextOpacity: 100, + BgOpacity: 100, + Size: "md", + } + + if value == nil { + return btn + } + + obj, ok := value.(map[string]any) + if !ok { + return btn + } + + // Content + if text, ok := obj["text"].(string); ok { + btn.Text = text + } + if url, ok := obj["url"].(string); ok { + btn.URL = url + } + + // Type - check explicit type field first + typeExplicitlySet := false + if t, ok := obj["type"].(string); ok && t != "" { + btn.Type = t + typeExplicitlySet = true + } + + // Use theme defaults toggle - explicit setting takes precedence + if useTheme, ok := obj["useThemeDefaults"].(bool); ok { + btn.UseThemeDefaults = useTheme + } else { + // Legacy: if useThemeDefaults not set but custom colors exist, assume not using theme defaults + if textColor, ok := obj["textColor"].(string); ok && textColor != "" { + btn.UseThemeDefaults = false + } else if bgColor, ok := obj["bgColor"].(string); ok && bgColor != "" { + btn.UseThemeDefaults = false + } + } + + // Custom overrides + if textColor, ok := obj["textColor"].(string); ok { + btn.TextColor = textColor + } + if textOpacity, ok := obj["textOpacity"].(float64); ok { + btn.TextOpacity = int(textOpacity) + } + if bgColor, ok := obj["bgColor"].(string); ok { + btn.BgColor = bgColor + } + if bgOpacity, ok := obj["bgOpacity"].(float64); ok { + btn.BgOpacity = int(bgOpacity) + } + + // Effects + if pill, ok := obj["pill"].(bool); ok { + btn.Pill = pill + } + if elevated, ok := obj["elevated"].(bool); ok { + btn.Elevated = elevated + } + if threeDee, ok := obj["threeDee"].(bool); ok { + btn.ThreeDee = threeDee + } + if size, ok := obj["size"].(string); ok && size != "" { + btn.Size = size + } + + // Behavior + if openNewTab, ok := obj["openNewTab"].(bool); ok { + btn.OpenNewTab = openNewTab + } + + // Legacy support: "style" field from old format + if style, ok := obj["style"].(string); ok && style != "" && btn.Type == defaultType { + // Map legacy styles to types + switch style { + case "secondary": + btn.Type = "secondary" + case "outline": + btn.Type = "outline" + case "ghost": + btn.Type = "ghost" + case "link": + btn.Type = "link" + case "destructive": + btn.Type = "destructive" + default: + btn.Type = "primary" + } + } + + // Legacy: "variant" field - only use if type wasn't explicitly set + if !typeExplicitlySet { + if variant, ok := obj["variant"].(string); ok && variant != "" { + if variant == "solid" { + btn.Type = "primary" + } else { + btn.Type = variant + } + } + } + + return btn +} + +// HasContent returns true if the button has text and URL +func (b ButtonConfig) HasContent() bool { + return b.Text != "" && b.URL != "" +} diff --git a/blocks/shared/button.templ b/blocks/shared/button.templ new file mode 100644 index 0000000..70f95f8 --- /dev/null +++ b/blocks/shared/button.templ @@ -0,0 +1,54 @@ +package shared + +// RenderButton renders a button or link based on ButtonConfig +templ RenderButton(btn ButtonConfig) { + if btn.URL != "" { + if btn.InlineStyle() != "" { + + { btn.Text } + + } else { + + { btn.Text } + + } + } else { + if btn.InlineStyle() != "" { + + } else { + + } + } +} + +// RenderSubmitButton renders a submit button +templ RenderSubmitButton(btn ButtonConfig) { + if btn.InlineStyle() != "" { + + } else { + + } +} diff --git a/blocks/shared/button_templ.go b/blocks/shared/button_templ.go new file mode 100644 index 0000000..15bafae --- /dev/null +++ b/blocks/shared/button_templ.go @@ -0,0 +1,370 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package shared + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +// RenderButton renders a button or link based on ButtonConfig +func RenderButton(btn ButtonConfig) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if btn.URL != "" { + if btn.InlineStyle() != "" { + var templ_7745c5c3_Var2 = []any{btn.Classes()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `blocks/shared/button.templ`, Line: 16, Col: 14} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + var templ_7745c5c3_Var7 = []any{btn.Classes()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(btn.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `blocks/shared/button.templ`, Line: 27, Col: 14} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else { + if btn.InlineStyle() != "" { + var templ_7745c5c3_Var11 = []any{btn.Classes()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + var templ_7745c5c3_Var15 = []any{btn.Classes()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + return nil + }) +} + +// RenderSubmitButton renders a submit button +func RenderSubmitButton(btn ButtonConfig) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var18 := templ.GetChildren(ctx) + if templ_7745c5c3_Var18 == nil { + templ_7745c5c3_Var18 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if btn.InlineStyle() != "" { + var templ_7745c5c3_Var19 = []any{btn.Classes()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + var templ_7745c5c3_Var23 = []any{btn.Classes()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/blocks/tagfilter.go b/blocks/tagfilter.go new file mode 100644 index 0000000..c3948ae --- /dev/null +++ b/blocks/tagfilter.go @@ -0,0 +1,99 @@ +package blocks + +import ( + "context" + "sort" +) + +// RenderContext is the render-time context handed to a plugin template tag. +// It is the standard context.Context that carries the active render values, +// so a tag fn reads page/post/author/request state with the same accessors +// blocks use everywhere else (blocks.GetCurrentPage(rctx), +// blocks.GetRequest(rctx), blocks.GetTemplateKey(rctx), …). +type RenderContext = context.Context + +// TagFunc renders a plugin-provided template tag. args holds the tag's parsed +// arguments; rctx is the active render context. It returns the tag's HTML. +type TagFunc func(args map[string]any, rctx RenderContext) (string, error) + +// FilterFunc applies a plugin-provided template filter to input. args holds +// the filter's arguments (e.g. {{ x|myfilter:"arg" }}). It returns the +// filtered output. +type FilterFunc func(input string, args map[string]any) (string, error) + +// Tag/filter registries are package-level singletons, mirroring +// RegisterTemplateRenderer: in the wasm guest exactly one plugin owns the +// module, so a global registry is the natural home for its RegisterTag / +// RegisterFilter calls. The guest snapshots names into the DESCRIBE manifest +// (declared_tags / declared_filters) and dispatches HOOK_RENDER_TAG / +// HOOK_APPLY_FILTER through LookupTag / LookupFilter. +var ( + registeredTags = map[string]TagFunc{} + registeredFilters = map[string]FilterFunc{} +) + +// RegisterTag registers a custom template tag under name. Call it from your +// plugin's Register func (alongside block registration) so DESCRIBE captures +// it into manifest.declared_tags and the host wires a pongo2 tag that calls +// back via HOOK_RENDER_TAG. A later registration of the same name wins. +func RegisterTag(name string, fn TagFunc) { + if name == "" || fn == nil { + return + } + registeredTags[name] = fn +} + +// RegisterFilter registers a custom template filter under name. Call it from +// your plugin's Register func so DESCRIBE captures it into +// manifest.declared_filters and the host wires a pongo2 filter that calls back +// via HOOK_APPLY_FILTER. A later registration of the same name wins. +func RegisterFilter(name string, fn FilterFunc) { + if name == "" || fn == nil { + return + } + registeredFilters[name] = fn +} + +// RegisteredTagNames returns the registered tag names, sorted (deterministic +// manifests). Read by the wasm guest's DESCRIBE handler. +func RegisteredTagNames() []string { + return sortedRegistryKeys(registeredTags) +} + +// RegisteredFilterNames returns the registered filter names, sorted. +func RegisteredFilterNames() []string { + return sortedRegistryKeys(registeredFilters) +} + +// LookupTag resolves a registered tag fn for HOOK_RENDER_TAG dispatch. +func LookupTag(name string) (TagFunc, bool) { + fn, ok := registeredTags[name] + return fn, ok +} + +// LookupFilter resolves a registered filter fn for HOOK_APPLY_FILTER dispatch. +func LookupFilter(name string) (FilterFunc, bool) { + fn, ok := registeredFilters[name] + return fn, ok +} + +// ResetRegisteredTagsAndFilters clears the tag/filter registries. The wasm +// guest calls it before running a registration's Register pass so each +// installed plugin starts from a clean slate (and so publish-time DESCRIBE is +// deterministic). Also used by tests. +func ResetRegisteredTagsAndFilters() { + registeredTags = map[string]TagFunc{} + registeredFilters = map[string]FilterFunc{} +} + +func sortedRegistryKeys[V any](m map[string]V) []string { + if len(m) == 0 { + return nil + } + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/blocks/tags/doc.go b/blocks/tags/doc.go new file mode 100644 index 0000000..2eee961 --- /dev/null +++ b/blocks/tags/doc.go @@ -0,0 +1 @@ +package tags diff --git a/blocks/template.go b/blocks/template.go new file mode 100644 index 0000000..0338201 --- /dev/null +++ b/blocks/template.go @@ -0,0 +1,74 @@ +package blocks + +import ( + "fmt" + "html" +) + +// MediaValue represents a rich media object for template substitution. +// Implements fmt.Stringer to render a full tag when used directly. +type MediaValue struct { + Src string // Full URL path (e.g., "/media/uuid/webp.webp") + Alt string // Alt text from media library + Filename string // Original filename + Width int // Image width in pixels + Height int // Image height in pixels +} + +// String renders the default tag representation. +func (m MediaValue) String() string { + attrs := fmt.Sprintf(`src="%s" alt="%s" class="w-full h-auto"`, + html.EscapeString(m.Src), + html.EscapeString(m.Alt)) + if m.Width > 0 && m.Height > 0 { + attrs += fmt.Sprintf(` width="%d" height="%d"`, m.Width, m.Height) + } + return fmt.Sprintf(``, attrs) +} + +// RenderWithClass renders the tag with custom CSS classes. +func (m MediaValue) RenderWithClass(class string) string { + attrs := fmt.Sprintf(`src="%s" alt="%s" class="%s"`, + html.EscapeString(m.Src), + html.EscapeString(m.Alt), + html.EscapeString(class)) + if m.Width > 0 && m.Height > 0 { + attrs += fmt.Sprintf(` width="%d" height="%d"`, m.Width, m.Height) + } + return fmt.Sprintf(``, attrs) +} + +// TemplateRenderer is the interface for rendering pongo2/Jinja2-style templates. +// The CMS registers an implementation at startup. +type TemplateRenderer interface { + RenderTemplate(tmpl string, data map[string]any) (string, error) + ProcessIcons(html string) string +} + +// templateRenderer holds the registered template renderer. +var templateRenderer TemplateRenderer + +// RegisterTemplateRenderer sets the global template renderer. +// Called by the CMS at startup to provide pongo2 rendering. +func RegisterTemplateRenderer(r TemplateRenderer) { + templateRenderer = r +} + +// RenderTemplate renders a pongo2/Jinja2-style template string with the given data. +// Returns the rendered HTML or an error. Falls back to a no-op if no renderer is registered. +func RenderTemplate(tmpl string, data map[string]any) (string, error) { + if templateRenderer == nil { + // No renderer registered — return template as-is (no variable substitution) + return tmpl, nil + } + return templateRenderer.RenderTemplate(tmpl, data) +} + +// ProcessIcons processes ::pack:name:: icon syntax in HTML. +// Returns the HTML with icons replaced, or the original if no renderer is registered. +func ProcessIcons(htmlStr string) string { + if templateRenderer == nil { + return htmlStr + } + return templateRenderer.ProcessIcons(htmlStr) +} diff --git a/blocks/types.go b/blocks/types.go new file mode 100644 index 0000000..c2b98f2 --- /dev/null +++ b/blocks/types.go @@ -0,0 +1,106 @@ +package blocks + +import ( + "context" + "fmt" + "strings" + + "github.com/google/uuid" +) + +// BlockFunc renders a block given its content data. +type BlockFunc func(ctx context.Context, content map[string]any) string + +// BlockCategory represents categories for organizing blocks in the palette. +type BlockCategory string + +const ( + CategoryContent BlockCategory = "content" + CategoryLayout BlockCategory = "layout" + CategoryNavigation BlockCategory = "navigation" + CategoryBlog BlockCategory = "blog" + CategoryTheme BlockCategory = "theme" +) + +// BlockMeta provides metadata about a block type for admin UI and validation. +type BlockMeta struct { + Key string + Title string + Description string + Category BlockCategory + HasInternalSlot bool + Source string + Hidden bool + EditorJS string +} + +// BlockNode represents a block with its content and children for rendering. +type BlockNode struct { + ID uuid.UUID + BlockKey string + Title string + Content map[string]any + HtmlContent string + Children []*BlockNode + SortOrder int +} + +// SlotRenderer is the interface for rendering container slots. +type SlotRenderer interface { + RenderContainerSlot(ctx context.Context, containerID uuid.UUID) string +} + +// HumanProofBannerData holds data for the public human proof banner. +type HumanProofBannerData struct { + ActiveTimeMinutes int + KeystrokeCount int + SessionCount int + PostSlug string +} + +// RenderHumanProofBanner renders the public-facing banner HTML. +func RenderHumanProofBanner(hp *HumanProofBannerData) string { + return fmt.Sprintf(`
`+ + `
`+ + `
`+ + `
HP
`+ + `
`+ + `
Human Proof
`+ + `
%[1]d min active · %[2]s keystrokes · %[3]d sessions
`+ + `
`+ + ``+ + `
`+ + ``+ + ``, + hp.ActiveTimeMinutes, + formatThousands(hp.KeystrokeCount), + hp.SessionCount, + hp.PostSlug, + ) +} + +func formatThousands(n int) string { + if n < 1000 { + return fmt.Sprintf("%d", n) + } + s := fmt.Sprintf("%d", n) + var result []byte + for i, c := range s { + if i > 0 && (len(s)-i)%3 == 0 { + result = append(result, ',') + } + result = append(result, byte(c)) + } + return string(result) +} + +// ResolveMediaPath converts a media: prefixed path to a /media/ URL path. +func ResolveMediaPath(path string) string { + if after, ok := strings.CutPrefix(path, "media:"); ok { + return "/media/" + after + } + if _, err := uuid.Parse(path); err == nil { + return "/media/" + path + } + return path +} diff --git a/content/author.go b/content/author.go new file mode 100644 index 0000000..f960e93 --- /dev/null +++ b/content/author.go @@ -0,0 +1,86 @@ +package content + +import ( + "context" + + "github.com/google/uuid" +) + +// Author provides imperative page/post authoring for plugins (WO-WZ-019). +// The CMS implements this interface and wires it into CoreServices. +// +// These are the runtime counterparts of the idempotent seed-time operations +// on plugin.Provisioner: CreatePage is create-or-return-existing, the rest +// mutate an existing page/post. Blog posts live in the CMS's dedicated +// blog_posts table (not the pages table). +type Author interface { + CreatePage(ctx context.Context, params CreatePageParams) (CreatePageResult, error) + SetPageBlocks(ctx context.Context, pageID uuid.UUID, blocks []PageBlock) error + PublishPage(ctx context.Context, pageID uuid.UUID) error + SetPageSEO(ctx context.Context, pageID uuid.UUID, seo PageSEO) error + UpsertPost(ctx context.Context, params UpsertPostParams) (UpsertPostResult, error) +} + +// CreatePageParams describes a page to create. +type CreatePageParams struct { + Slug string + ParentSlug string // "" = root + Title string + TemplateKey string + MasterPageKey string // "" = none +} + +// CreatePageResult reports the created (or pre-existing) page. +type CreatePageResult struct { + PageID uuid.UUID + // Created is false when a page with the slug already existed; the + // existing ID is returned and nothing is modified. + Created bool +} + +// PageBlock is one block instance placed on a page. +type PageBlock struct { + BlockKey string + Title string + Content map[string]any + HTMLContent *string + Slot string + SortOrder int32 +} + +// PageSEO carries the SEO fields settable on a page. Nil pointers leave the +// existing value untouched. +type PageSEO struct { + MetaTitle *string + MetaDescription *string + OGTitle *string + OGDescription *string + OGImage *string + FocusKeyphrase *string + CanonicalURL *string + RobotsDirective *string + TwitterTitle *string + TwitterDescription *string + TwitterImage *string +} + +// UpsertPostParams creates or updates a blog post by slug. +type UpsertPostParams struct { + Slug string + Title string + Document map[string]any // BlockNote document + Excerpt *string + // AuthorProfileID / FeaturedImageID reference existing rows (e.g. a + // media.deposit result for the image). + AuthorProfileID *uuid.UUID + FeaturedImageID *uuid.UUID + // Publish publishes the post immediately after the upsert. + Publish bool + IsFeatured bool +} + +// UpsertPostResult reports the upserted post. +type UpsertPostResult struct { + PostID uuid.UUID + Created bool +} diff --git a/content/content.go b/content/content.go new file mode 100644 index 0000000..b022f5d --- /dev/null +++ b/content/content.go @@ -0,0 +1,68 @@ +package content + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +// AuthorProfile is a simplified author representation for plugins. +type AuthorProfile struct { + ID uuid.UUID + Name string + Slug string + Bio string + AvatarURL string + Website string + SocialLinks map[string]string +} + +// PageInfo is a simplified page representation for plugins. +type PageInfo struct { + ID uuid.UUID + Slug string + Title string +} + +// PostInfo is a simplified post representation for plugins. It carries enough +// for both a blog index (title/slug/excerpt/author/date/featured image) and a +// post detail view (Body — the rendered HTML). Body is populated by GetPost and +// by ListPosts when ListPostsParams.IncludeBody is set; it is empty otherwise. +type PostInfo struct { + ID uuid.UUID + Slug string + Title string + Excerpt string + Body string // rendered HTML; populated by GetPost / ListPosts(IncludeBody) + FeaturedImageURL string + AuthorID uuid.UUID + AuthorName string + AuthorSlug string + PublishedAt time.Time +} + +// ListPostsParams filters and shapes a ListPosts query. The zero value lists +// published posts from the start with no category filter, excerpts included and +// bodies omitted (the cheap blog-index shape). +type ListPostsParams struct { + Limit int // 0 = host default + Offset int // pagination offset + Category string // optional category slug filter ("" = all) + PublishedOnly bool // reserved: plugins only ever see published posts + IncludeBody bool // render each post's body to HTML (expensive) + IncludeExcerpt bool // include the post excerpt +} + +// Content provides content access for plugins. +// The CMS implements this interface and wires it into CoreServices. +type Content interface { + GetAuthorProfile(ctx context.Context, id uuid.UUID) (*AuthorProfile, error) + GetPage(ctx context.Context, slug string) (*PageInfo, error) + GetPost(ctx context.Context, slug string) (*PostInfo, error) + ListPosts(ctx context.Context, params ListPostsParams) ([]PostInfo, error) + Slugify(text string) string + BlockNoteToHTML(ctx context.Context, doc map[string]any) string + GenerateExcerpt(html string, maxLen int) string + StripHTML(s string) string +} diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 0000000..9afbf2a --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,7 @@ +package crypto + +// Crypto provides encryption/decryption for plugins. +type Crypto interface { + EncryptSecret(plaintext string) (string, error) + DecryptSecret(ciphertext string) (string, error) +} diff --git a/datasources/datasources.go b/datasources/datasources.go new file mode 100644 index 0000000..8bbd5e7 --- /dev/null +++ b/datasources/datasources.go @@ -0,0 +1,21 @@ +package datasources + +import ( + "context" + + "github.com/google/uuid" +) + +// Datasources provides bucket/datasource access for plugins. +// The CMS implements this interface and wires it into CoreServices. +type Datasources interface { + ResolveBucket(ctx context.Context, bucketID uuid.UUID) (*Result, error) + ResolveBucketByKey(ctx context.Context, bucketKey string) (*Result, error) +} + +// Result is the output from resolving a bucket. +type Result struct { + Items []any `json:"items"` + Total int `json:"total"` + Meta map[string]any `json:"meta,omitempty"` +} diff --git a/gating/gating.go b/gating/gating.go new file mode 100644 index 0000000..2e14465 --- /dev/null +++ b/gating/gating.go @@ -0,0 +1,45 @@ +package gating + +import ( + "context" + + "github.com/google/uuid" +) + +// AccessRule defines content access requirements. +type AccessRule struct { + MinTierLevel int + OverrideTierID string + TeaserMode string // "hard", "soft", "none" + TeaserPercent int +} + +// AccessResult is the result of evaluating access for a user. +type AccessResult struct { + HasAccess bool + TeaserMode string + TeaserPercent int + RequiredLevel int +} + +// EvaluateAccess checks whether a user's tier level grants access based on the rule. +func EvaluateAccess(userTierLevel int, rule *AccessRule) AccessResult { + if rule == nil { + return AccessResult{HasAccess: true} + } + if userTierLevel >= rule.MinTierLevel { + return AccessResult{HasAccess: true} + } + return AccessResult{ + HasAccess: false, + TeaserMode: rule.TeaserMode, + TeaserPercent: rule.TeaserPercent, + RequiredLevel: rule.MinTierLevel, + } +} + +// Gating provides gating access for plugins. +type Gating interface { + GetSubscriberTierLevel(ctx context.Context, userID uuid.UUID) (int, error) + EvaluateAccess(userTierLevel int, rule *AccessRule) AccessResult +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..935fd8b --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module git.dev.alexdunmow.com/block/pluginsdk + +go 1.26.4 + +require ( + connectrpc.com/connect v1.20.0 + git.dev.alexdunmow.com/block/ninjatpl v1.0.2 + github.com/BurntSushi/toml v1.6.0 + github.com/a-h/templ v0.3.1020 + github.com/google/uuid v1.6.0 + github.com/jackc/pgx/v5 v5.10.0 + github.com/tetratelabs/wazero v1.12.0 + golang.org/x/mod v0.37.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..89849f8 --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +git.dev.alexdunmow.com/block/ninjatpl v1.0.2 h1:KQS90HNW35PSzvwDZ3Z9OOwX3OSjX0x62w0AtOYps8s= +git.dev.alexdunmow.com/block/ninjatpl v1.0.2/go.mod h1:WrLz0KysnP5EzJlRCkU2VC1uvazyAXZR99Tn+3Mx1pw= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= +github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/menus/menus.go b/menus/menus.go new file mode 100644 index 0000000..3b0cba1 --- /dev/null +++ b/menus/menus.go @@ -0,0 +1,34 @@ +package menus + +import ( + "context" + + "github.com/google/uuid" +) + +// Menus provides menu and navigation access for plugins. +type Menus interface { + GetMenuByName(ctx context.Context, name string) (*Menu, error) + GetMenuItems(ctx context.Context, menuID uuid.UUID) ([]MenuItem, error) +} + +// Menu represents a named navigation menu. +type Menu struct { + ID uuid.UUID + Name string +} + +// MenuItem represents a single entry in a navigation menu. +type MenuItem struct { + ID uuid.UUID + MenuID uuid.UUID + Label string + URL string + PageSlug string + ParentID *uuid.UUID + SortOrder int32 + OpenInNewTab bool + CssClass string + ItemType string + Icon string +} diff --git a/plugin/block_registry.go b/plugin/block_registry.go new file mode 100644 index 0000000..fdb5866 --- /dev/null +++ b/plugin/block_registry.go @@ -0,0 +1,43 @@ +package plugin + +import ( + "io/fs" + "strings" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" +) + +// PluginBlockRegistry wraps a BlockRegistry to auto-prefix block keys with the plugin name. +type PluginBlockRegistry struct { + inner blocks.BlockRegistry + prefix string +} + +// NewPluginBlockRegistry creates a block registry that auto-prefixes keys. +func NewPluginBlockRegistry(inner blocks.BlockRegistry, pluginName string) *PluginBlockRegistry { + return &PluginBlockRegistry{inner: inner, prefix: pluginName} +} + +func (r *PluginBlockRegistry) Register(meta blocks.BlockMeta, fn blocks.BlockFunc) { + if !strings.Contains(meta.Key, ":") { + meta.Key = r.prefix + ":" + meta.Key + } + meta.Source = r.prefix + r.inner.Register(meta, fn) +} + +func (r *PluginBlockRegistry) RegisterTemplateOverride(templateKey, blockKey string, fn blocks.BlockFunc) { + r.inner.RegisterTemplateOverrideWithSource(templateKey, blockKey, r.prefix, fn) +} + +func (r *PluginBlockRegistry) RegisterTemplateOverrideWithSource(templateKey, blockKey, source string, fn blocks.BlockFunc) { + r.inner.RegisterTemplateOverrideWithSource(templateKey, blockKey, source, fn) +} + +func (r *PluginBlockRegistry) LoadSchemasFromFS(fsys fs.FS) error { + return r.inner.LoadSchemasFromFSWithPrefix(fsys, r.prefix) +} + +func (r *PluginBlockRegistry) LoadSchemasFromFSWithPrefix(fsys fs.FS, prefix string) error { + return r.inner.LoadSchemasFromFSWithPrefix(fsys, prefix) +} diff --git a/plugin/block_registry_test.go b/plugin/block_registry_test.go new file mode 100644 index 0000000..b36d60d --- /dev/null +++ b/plugin/block_registry_test.go @@ -0,0 +1,70 @@ +package plugin + +import ( + "io/fs" + "testing" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" +) + +type recordingBlockRegistry struct { + registeredKey string + registeredSource string + overrideSource string + schemaPrefix string +} + +func (r *recordingBlockRegistry) Register(meta blocks.BlockMeta, _ blocks.BlockFunc) { + r.registeredKey = meta.Key + r.registeredSource = meta.Source +} + +func (r *recordingBlockRegistry) RegisterTemplateOverride(_, _ string, _ blocks.BlockFunc) {} + +func (r *recordingBlockRegistry) RegisterTemplateOverrideWithSource(_, _, source string, _ blocks.BlockFunc) { + r.overrideSource = source +} + +func (r *recordingBlockRegistry) LoadSchemasFromFS(fsys fs.FS) error { + return r.LoadSchemasFromFSWithPrefix(fsys, "") +} + +func (r *recordingBlockRegistry) LoadSchemasFromFSWithPrefix(_ fs.FS, prefix string) error { + r.schemaPrefix = prefix + return nil +} + +func TestPluginBlockRegistryPrefixesOnlyUnqualifiedKeys(t *testing.T) { + inner := &recordingBlockRegistry{} + registry := NewPluginBlockRegistry(inner, "course") + + registry.Register(blocks.BlockMeta{Key: "lesson"}, nil) + if inner.registeredKey != "course:lesson" { + t.Fatalf("Register() key = %q, want course:lesson", inner.registeredKey) + } + if inner.registeredSource != "course" { + t.Fatalf("Register() source = %q, want course", inner.registeredSource) + } + + registry.Register(blocks.BlockMeta{Key: "course:lesson"}, nil) + if inner.registeredKey != "course:lesson" { + t.Fatalf("Register() double-prefixed qualified key: %q", inner.registeredKey) + } +} + +func TestPluginBlockRegistryTracksOverrideAndSchemaSource(t *testing.T) { + inner := &recordingBlockRegistry{} + registry := NewPluginBlockRegistry(inner, "course") + + registry.RegisterTemplateOverride("shared-theme", "lesson", nil) + if inner.overrideSource != "course" { + t.Fatalf("override source = %q, want course", inner.overrideSource) + } + + if err := registry.LoadSchemasFromFS(nil); err != nil { + t.Fatalf("LoadSchemasFromFS() error = %v", err) + } + if inner.schemaPrefix != "course" { + t.Fatalf("schema prefix = %q, want course", inner.schemaPrefix) + } +} diff --git a/plugin/bridge.go b/plugin/bridge.go new file mode 100644 index 0000000..6825f3a --- /dev/null +++ b/plugin/bridge.go @@ -0,0 +1,42 @@ +package plugin + +import "context" + +// PluginBridge allows plugins to share services with each other. +// Plugins register named services during startup; other plugins look them up at runtime. +// +// In-process (bundled) plugins can share typed Go values via +// RegisterService/GetService. Across the wasm sandbox a typed value cannot +// cross, so cross-plugin calls go through Invoke instead: the provider's +// service implements BridgeInvokable and the consumer calls Invoke with an +// opaque payload (JSON by convention). GetService still reports availability +// only (nil) for wasm consumers. +type PluginBridge interface { + RegisterService(pluginName, serviceName string, service any) + GetService(pluginName, serviceName string) any + // Invoke calls a method on another plugin's registered bridge service. + // The provider answers via its BridgeInvokable implementation (bundled) + // or the BRIDGE_CALL hook (wasm). + Invoke(ctx context.Context, pluginName, serviceName, method string, payload []byte) ([]byte, error) +} + +// BridgeInvokable is implemented by bridge service values that support +// method-by-name invocation with opaque payloads, making them callable from +// other plugins across the wasm sandbox boundary. +type BridgeInvokable interface { + InvokeBridge(ctx context.Context, method string, payload []byte) ([]byte, error) +} + +// GetServiceAs retrieves a typed service from the bridge. +func GetServiceAs[T any](bridge PluginBridge, pluginName, serviceName string) (T, bool) { + var zero T + if bridge == nil { + return zero, false + } + svc := bridge.GetService(pluginName, serviceName) + if svc == nil { + return zero, false + } + typed, ok := svc.(T) + return typed, ok +} diff --git a/plugin/community.go b/plugin/community.go new file mode 100644 index 0000000..8935f8c --- /dev/null +++ b/plugin/community.go @@ -0,0 +1,13 @@ +package plugin + +import ( + "git.dev.alexdunmow.com/block/pluginsdk/rbac" +) + +// CoreServiceBindings provides pre-built CMS service bindings that plugins +// can include in their ServiceRegistration with custom RBAC role mappings. +// The CMS constructs the actual service handlers — plugins just specify which +// services to mount and what roles apply. +type CoreServiceBindings interface { + Bind(serviceName string, methodRoles map[string]rbac.Role) (ConnectServiceBinding, error) +} diff --git a/plugin/css_manifest.go b/plugin/css_manifest.go new file mode 100644 index 0000000..9da1495 --- /dev/null +++ b/plugin/css_manifest.go @@ -0,0 +1,44 @@ +package plugin + +import "maps" + +// CSSManifest declares a plugin's CSS dependencies and customizations. +type CSSManifest struct { + NPMPackages map[string]string `json:"npm_packages,omitempty"` + CSSDirectives []string `json:"css_directives,omitempty"` + InputCSSAppend string `json:"input_css_append,omitempty"` +} + +// MergedCSSManifest aggregates CSS manifests from all plugins. +type MergedCSSManifest struct { + NPMPackages map[string]string `json:"npm_packages"` + CSSDirectives []string `json:"css_directives"` + InputCSSAppend string `json:"input_css_append"` + Sources []string `json:"sources"` +} + +// MergeCSSManifests combines multiple plugin CSS manifests into one. +func MergeCSSManifests(manifests map[string]*CSSManifest) *MergedCSSManifest { + merged := &MergedCSSManifest{ + NPMPackages: make(map[string]string), + } + + for name, m := range manifests { + if m == nil { + continue + } + merged.Sources = append(merged.Sources, name) + maps.Copy(merged.NPMPackages, m.NPMPackages) + merged.CSSDirectives = append(merged.CSSDirectives, m.CSSDirectives...) + + if m.InputCSSAppend != "" { + if merged.InputCSSAppend != "" { + merged.InputCSSAppend += "\n" + } + merged.InputCSSAppend += "/* === Plugin: " + name + " === */\n" + merged.InputCSSAppend += m.InputCSSAppend + } + } + + return merged +} diff --git a/plugin/deps.go b/plugin/deps.go new file mode 100644 index 0000000..6129fee --- /dev/null +++ b/plugin/deps.go @@ -0,0 +1,147 @@ +package plugin + +import ( + "context" + + "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/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 + 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 + + // Core RPC services — pre-built bindings for CMS-provided services + CoreServiceBindings CoreServiceBindings + ReviewSubmitter ReviewSubmitter + BadgeRefresher BadgeRefresher + SettingsUpdater settings.Updater + + // 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:") 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 +} + +// 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 +} diff --git a/plugin/mod.go b/plugin/mod.go new file mode 100644 index 0000000..9a4e035 --- /dev/null +++ b/plugin/mod.go @@ -0,0 +1,150 @@ +package plugin + +import ( + "fmt" + "regexp" + "strings" + + tomlpkg "github.com/BurntSushi/toml" +) + +type ModFile struct { + Plugin ModPlugin `toml:"plugin"` + Compatibility *ModCompat `toml:"compatibility"` + Requires []ModRequirement `toml:"requires"` +} + +type ModPlugin struct { + // Name is the lowercase identifier used for the plugin slug in URLs and DB + // lookups. The CLI normalises this on write; the registry normalises on + // create. Use DisplayName for human-readable presentation. + Name string `toml:"name"` + // DisplayName is the human-readable form (any case). Optional; if empty + // the registry falls back to the input name with its original case. + DisplayName string `toml:"display_name,omitempty"` + // Description is the short summary surfaced in the registry. Optional. + Description string `toml:"description,omitempty"` + // Scope is the plugin owner namespace as it appears in plugin.mod. It may + // include the leading "@" (e.g. "@themes") or omit it (e.g. "themes") — + // both forms are accepted. Consumers comparing scopes should trim the "@" + // before comparing; use ModFile.Coords() for a normalised display string. + Scope string `toml:"scope"` + Version string `toml:"version"` + Kind string `toml:"kind,omitempty"` + Categories []string `toml:"categories,omitempty"` + Tags []string `toml:"tags,omitempty"` + // RequiredIconPacks names icon-pack slugs the host CMS must ensure are + // installed before the plugin is loaded (e.g. "tabler", "phosphor"). The + // standalone-plugin loader honours this best-effort by auto-installing any + // missing packs from the bundled registry; slugs outside that whitelist are + // logged and skipped (admins install them manually). Empty / omitted means + // the plugin has no icon-pack dependencies. + RequiredIconPacks []string `toml:"required_icon_packs,omitempty"` + // Private marks the plugin as account-scoped. When true, Coords() returns + // the canonical "@private/@" form regardless of the Scope + // field, and the publish flow attributes the plugin to the publisher's + // active account rather than to a public scope. + Private bool `toml:"private,omitempty"` + // DataDir requests a persistent per-plugin /data preopen at load time. It + // is a first-class field (not an arbitrary key) precisely so the CLI's + // mod round-trip cannot silently drop it: writeMod reconstructs plugin.mod + // from struct fields only, so any key without a home here is lost on the + // next `ninja plugin init`/bump. `ninja plugin build` stamps this into the + // packed manifest (PluginManifest.data_dir); the .bnp reader also reads it + // straight from plugin.mod as a fallback. OFF by default. + DataDir bool `toml:"data_dir,omitempty"` +} + +type ModCompat struct { + BlockCore string `toml:"block_core"` +} + +type ModRequirement struct { + Name string `toml:"name"` + Version string `toml:"version"` +} + +func ParseModFull(b []byte) (*ModFile, error) { + var m ModFile + if err := tomlpkg.Unmarshal(b, &m); err != nil { + return nil, err + } + return &m, nil +} + +// Coords returns the canonical display coordinate for the plugin in the form +// "@scope/name@version" (or "name@version" when no scope is set). +// +// The leading "@" on m.Plugin.Scope is intentionally trimmed before +// re-prefixing so that authors may write either "@themes" or "themes" in +// plugin.mod and get the same output. Callers that need the raw scope as +// written should read m.Plugin.Scope directly. +func (m *ModFile) Coords() string { + if m == nil { + return "" + } + if m.Plugin.Private { + return "@" + PrivateScopeSlug + "/" + m.Plugin.Name + "@" + m.Plugin.Version + } + scope := strings.TrimPrefix(m.Plugin.Scope, "@") + if scope == "" { + return m.Plugin.Name + "@" + m.Plugin.Version + } + return "@" + scope + "/" + m.Plugin.Name + "@" + m.Plugin.Version +} + +// PrivateScopeSlug is the registry namespace under which all private plugins +// live. Coords for private plugins resolve to "@private/@"; +// uniqueness is enforced by (owner_account_id, name), not by the slug. +const PrivateScopeSlug = "private" + +const ( + TagMinLen = 2 + TagMaxLen = 30 + TagMaxCount = 10 +) + +// tagSlugRe matches lowercase a-z, 0-9, with single hyphens between groups. +// Rejects leading/trailing/consecutive hyphens. +var tagSlugRe = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) + +// NormalizeTags trims, lowercases, dedupes (case-insensitively), validates, +// and caps a slice of tags. Returns the cleaned slice or an error listing +// every offending input so authors fix them in one pass. +// +// Rules: +// - trim surrounding whitespace; drop empty entries silently +// - lowercase +// - require [a-z0-9-]{TagMinLen..TagMaxLen}, no leading/trailing/consecutive hyphens +// - dedupe case-insensitively, preserving first occurrence order +// - at most TagMaxCount entries (counted after dedupe) +func NormalizeTags(in []string) ([]string, error) { + seen := make(map[string]struct{}, len(in)) + out := make([]string, 0, len(in)) + var bad []string + for _, raw := range in { + t := strings.ToLower(strings.TrimSpace(raw)) + if t == "" { + continue + } + if _, dup := seen[t]; dup { + continue + } + if len(t) < TagMinLen || len(t) > TagMaxLen || !tagSlugRe.MatchString(t) { + bad = append(bad, raw) + continue + } + seen[t] = struct{}{} + out = append(out, t) + } + if len(bad) > 0 { + return nil, fmt.Errorf( + "invalid tags (must be %d-%d chars, lowercase a-z 0-9 and single hyphens, no leading/trailing hyphen): %s", + TagMinLen, TagMaxLen, strings.Join(bad, ", "), + ) + } + if len(out) > TagMaxCount { + return nil, fmt.Errorf("too many tags: got %d, max %d", len(out), TagMaxCount) + } + return out, nil +} diff --git a/plugin/mod_test.go b/plugin/mod_test.go new file mode 100644 index 0000000..7eaa9ce --- /dev/null +++ b/plugin/mod_test.go @@ -0,0 +1,434 @@ +package plugin + +import ( + "fmt" + "strings" + "testing" +) + +func TestParseModFull_BasicFields(t *testing.T) { + src := []byte(` +[plugin] +name = "smartblock" +scope = "blockninja" +version = "1.4.2" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.Name != "smartblock" { + t.Errorf("Name = %q, want smartblock", m.Plugin.Name) + } + if m.Plugin.Scope != "blockninja" { + t.Errorf("Scope = %q, want blockninja", m.Plugin.Scope) + } + if m.Plugin.Version != "1.4.2" { + t.Errorf("Version = %q, want 1.4.2", m.Plugin.Version) + } + if got := m.Coords(); got != "@blockninja/smartblock@1.4.2" { + t.Errorf("Coords() = %q", got) + } +} + +func TestParseModFull_BackCompatNoScope(t *testing.T) { + src := []byte(` +[plugin] +name = "legacy" +version = "0.1.0" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.Scope != "" { + t.Errorf("Scope should be empty, got %q", m.Plugin.Scope) + } + if got := m.Coords(); got != "legacy@0.1.0" { + t.Errorf("Coords() = %q, want legacy@0.1.0", got) + } +} + +func TestParseModFull_InvalidTOML(t *testing.T) { + _, err := ParseModFull([]byte("not valid toml = =")) + if err == nil { + t.Fatal("expected parse error") + } +} + +func TestParseModFull_EmptyInput(t *testing.T) { + m, err := ParseModFull(nil) + if err != nil { + t.Fatalf("nil input err: %v", err) + } + if m.Plugin.Name != "" { + t.Errorf("Name should be empty") + } +} + +func TestParseModFull_KindAndCategories(t *testing.T) { + src := []byte(` +[plugin] +name = "analyser" +scope = "blockninja" +version = "0.1.0" +kind = "plugin" +categories = ["analytics", "seo"] +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.Kind != "plugin" { + t.Errorf("Kind = %q, want plugin", m.Plugin.Kind) + } + if got := m.Plugin.Categories; len(got) != 2 || got[0] != "analytics" || got[1] != "seo" { + t.Errorf("Categories = %v", got) + } +} + +func TestParseModFull_KindDefaultsEmpty(t *testing.T) { + src := []byte(` +[plugin] +name = "legacy" +version = "0.1.0" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.Kind != "" { + t.Errorf("Kind should be empty for legacy mod, got %q", m.Plugin.Kind) + } +} + +func TestCoords_AcceptsScopeWithOrWithoutAt(t *testing.T) { + want := "@themes/foo@1.0.0" + + withAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "@themes", Version: "1.0.0"}} + if got := withAt.Coords(); got != want { + t.Errorf("Coords() with leading @ = %q, want %q", got, want) + } + + withoutAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "themes", Version: "1.0.0"}} + if got := withoutAt.Coords(); got != want { + t.Errorf("Coords() without leading @ = %q, want %q", got, want) + } +} + +func TestParseModFull_PrivateField(t *testing.T) { + src := []byte(` +[plugin] +name = "internal-tool" +version = "0.1.0" +private = true +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if !m.Plugin.Private { + t.Errorf("Private = false, want true") + } +} + +func TestParseModFull_DataDirField(t *testing.T) { + src := []byte(` +[plugin] +name = "stateful-plugin" +version = "0.1.0" +data_dir = true +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if !m.Plugin.DataDir { + t.Errorf("DataDir = false, want true") + } +} + +func TestParseModFull_DataDirDefaultsFalse(t *testing.T) { + src := []byte(` +[plugin] +name = "stateless-plugin" +version = "0.1.0" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.DataDir { + t.Errorf("DataDir = true, want false (absent key)") + } +} + +func TestParseModFull_PrivateDefaultsFalse(t *testing.T) { + src := []byte(` +[plugin] +name = "public-thing" +scope = "themes" +version = "0.1.0" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Plugin.Private { + t.Errorf("Private = true, want false (default)") + } +} + +func TestCoords_PrivateOverridesScope(t *testing.T) { + m := &ModFile{Plugin: ModPlugin{ + Name: "myplugin", + Scope: "@themes", + Version: "0.1.0", + Private: true, + }} + if got := m.Coords(); got != "@private/myplugin@0.1.0" { + t.Errorf("Coords() = %q, want @private/myplugin@0.1.0", got) + } +} + +func TestCoords_PrivateNoScope(t *testing.T) { + m := &ModFile{Plugin: ModPlugin{ + Name: "myplugin", + Version: "0.1.0", + Private: true, + }} + if got := m.Coords(); got != "@private/myplugin@0.1.0" { + t.Errorf("Coords() = %q, want @private/myplugin@0.1.0", got) + } +} + +func TestParseModFull_RequiredIconPacks(t *testing.T) { + src := []byte(` +[plugin] +name = "neon" +version = "0.1.0" +required_icon_packs = ["tabler", "phosphor"] +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if len(m.Plugin.RequiredIconPacks) != 2 { + t.Fatalf("RequiredIconPacks len = %d, want 2", len(m.Plugin.RequiredIconPacks)) + } + if m.Plugin.RequiredIconPacks[0] != "tabler" || m.Plugin.RequiredIconPacks[1] != "phosphor" { + t.Errorf("RequiredIconPacks = %v", m.Plugin.RequiredIconPacks) + } +} + +func TestParseModFull_RequiredIconPacksOmitted(t *testing.T) { + src := []byte(` +[plugin] +name = "noicons" +version = "0.1.0" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if len(m.Plugin.RequiredIconPacks) != 0 { + t.Errorf("RequiredIconPacks should be empty when omitted, got %v", m.Plugin.RequiredIconPacks) + } +} + +func TestParseRequiredIconPacks(t *testing.T) { + src := []byte(` +[plugin] +name = "neon" +version = "0.1.0" +required_icon_packs = ["tabler", " phosphor ", ""] +`) + got := ParseRequiredIconPacks(src) + if len(got) != 2 { + t.Fatalf("ParseRequiredIconPacks len = %d (%v), want 2", len(got), got) + } + if got[0] != "tabler" || got[1] != "phosphor" { + t.Errorf("ParseRequiredIconPacks = %v, want [tabler phosphor]", got) + } +} + +func TestParseRequiredIconPacks_NilOnAbsent(t *testing.T) { + src := []byte(` +[plugin] +name = "noicons" +version = "0.1.0" +`) + if got := ParseRequiredIconPacks(src); got != nil { + t.Errorf("ParseRequiredIconPacks on absent field = %v, want nil", got) + } +} + +func TestParseRequiredIconPacks_NilOnInvalidTOML(t *testing.T) { + if got := ParseRequiredIconPacks([]byte("not valid = = =")); got != nil { + t.Errorf("ParseRequiredIconPacks on invalid TOML = %v, want nil", got) + } +} + +func TestParseModFull_RequiresAndCompat(t *testing.T) { + src := []byte(` +[plugin] +name = "symposium" +scope = "blockninja" +version = "0.2.0" + +[compatibility] +block_core = ">=1.5 <2.0" + +[[requires]] +name = "@blockninja/smartblock" +version = ">=1.0 <2.0" + +[[requires]] +name = "@blockninja/gotham" +version = ">=1.2" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if m.Compatibility == nil || m.Compatibility.BlockCore != ">=1.5 <2.0" { + t.Errorf("Compat = %+v", m.Compatibility) + } + if len(m.Requires) != 2 { + t.Fatalf("Requires len = %d, want 2", len(m.Requires)) + } + if m.Requires[0].Name != "@blockninja/smartblock" { + t.Errorf("Requires[0].Name = %q", m.Requires[0].Name) + } + if m.Requires[1].Version != ">=1.2" { + t.Errorf("Requires[1].Version = %q", m.Requires[1].Version) + } +} + +func TestNormalizeTags_HappyPath(t *testing.T) { + got, err := NormalizeTags([]string{"dark", "agency", "serif"}) + if err != nil { + t.Fatalf("NormalizeTags err: %v", err) + } + want := []string{"dark", "agency", "serif"} + if len(got) != len(want) { + t.Fatalf("len = %d, want %d (%v)", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("[%d] = %q, want %q", i, got[i], want[i]) + } + } +} + +func TestNormalizeTags_LowercaseAndTrim(t *testing.T) { + got, err := NormalizeTags([]string{" Dark ", "AGENCY"}) + if err != nil { + t.Fatalf("err: %v", err) + } + if len(got) != 2 || got[0] != "dark" || got[1] != "agency" { + t.Errorf("got %v, want [dark agency]", got) + } +} + +func TestNormalizeTags_DedupesCaseInsensitive(t *testing.T) { + got, err := NormalizeTags([]string{"dark", "Dark", "DARK", "agency"}) + if err != nil { + t.Fatalf("err: %v", err) + } + if len(got) != 2 || got[0] != "dark" || got[1] != "agency" { + t.Errorf("got %v, want [dark agency]", got) + } +} + +func TestNormalizeTags_DropsEmpty(t *testing.T) { + got, err := NormalizeTags([]string{"", "dark", " "}) + if err != nil { + t.Fatalf("err: %v", err) + } + if len(got) != 1 || got[0] != "dark" { + t.Errorf("got %v, want [dark]", got) + } +} + +func TestNormalizeTags_RejectsBadSlugs(t *testing.T) { + _, err := NormalizeTags([]string{"valid", "Has Space", "trailing-", "-leading", "double--hyphen", "a", strings.Repeat("x", 31)}) + if err == nil { + t.Fatal("expected error") + } + for _, frag := range []string{"Has Space", "trailing-", "-leading", "double--hyphen", "a", strings.Repeat("x", 31)} { + if !strings.Contains(err.Error(), frag) { + t.Errorf("error %q does not mention %q", err.Error(), frag) + } + } +} + +func TestNormalizeTags_AcceptsBounds(t *testing.T) { + got, err := NormalizeTags([]string{"ab", strings.Repeat("a", 30)}) + if err != nil { + t.Fatalf("err: %v", err) + } + if len(got) != 2 { + t.Errorf("got %v, want both accepted", got) + } +} + +func TestNormalizeTags_CapEnforced(t *testing.T) { + in := make([]string, TagMaxCount+1) + for i := range in { + in[i] = fmt.Sprintf("tag-%d", i) + } + _, err := NormalizeTags(in) + if err == nil { + t.Fatal("expected too-many-tags error") + } + if !strings.Contains(err.Error(), "too many tags") { + t.Errorf("error %q does not mention 'too many tags'", err.Error()) + } +} + +func TestNormalizeTags_NilInput(t *testing.T) { + got, err := NormalizeTags(nil) + if err != nil { + t.Fatalf("err: %v", err) + } + if len(got) != 0 { + t.Errorf("got %v, want empty", got) + } +} + +func TestParseModFull_Tags(t *testing.T) { + src := []byte(` +[plugin] +name = "dark-pro" +scope = "themes" +version = "0.1.0" +kind = "theme" +tags = ["dark", "agency", "serif"] +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("ParseModFull err: %v", err) + } + if len(m.Plugin.Tags) != 3 { + t.Fatalf("Tags len = %d, want 3 (%v)", len(m.Plugin.Tags), m.Plugin.Tags) + } + if m.Plugin.Tags[0] != "dark" || m.Plugin.Tags[1] != "agency" || m.Plugin.Tags[2] != "serif" { + t.Errorf("Tags = %v", m.Plugin.Tags) + } +} + +func TestParseModFull_TagsOmittedIsNil(t *testing.T) { + src := []byte(` +[plugin] +name = "no-tags" +version = "0.1.0" +`) + m, err := ParseModFull(src) + if err != nil { + t.Fatalf("err: %v", err) + } + if m.Plugin.Tags != nil { + t.Errorf("Tags = %v, want nil when omitted", m.Plugin.Tags) + } +} diff --git a/plugin/provisioner.go b/plugin/provisioner.go new file mode 100644 index 0000000..91f5eb0 --- /dev/null +++ b/plugin/provisioner.go @@ -0,0 +1,112 @@ +package plugin + +import ( + "context" + "encoding/json" + + "github.com/google/uuid" +) + +// Provisioner allows plugins to ensure required resources exist on startup. +// All methods are idempotent — they check first and only create if missing. +type Provisioner interface { + EnsureDataTable(config DataTableConfig) error + MergeSiteSettings(defaults map[string]any) error + EnsureSetting(key string, defaultValue any) error + EnsurePage(config PageConfig) error + OverrideSiteSettings(overrides map[string]any) error + EnsureMenuItem(menuName string, config MenuItemConfig) error + RegisterEmbeddingConfig(config EmbeddingConfigDef) error + EnsureEmbed(config EmbedConfig) error + EnsureJobSchedule(config JobScheduleConfig) error + UpdateDataTableRowField(ctx context.Context, rowID uuid.UUID, fieldKey string, value any) error + DisableOrphanedJobSchedules(registeredTypes []string) error + EnsurePlugin(name string) error + EnsureCustomColor(config CustomColorConfig) error + // EnsureMedia deposits an image under the plugin-chosen MediaDeposit.ID if a + // row with that ID does not already exist. Idempotent: an existing ID is a + // no-op; if the bytes differ from what was first deposited it logs a warning + // rather than overwriting (seeded media is immutable once placed). + EnsureMedia(deposit MediaDeposit) error +} + +// DataTableConfig defines a data table to provision. +type DataTableConfig struct { + Key string + Name string + Description string + Schema json.RawMessage + PrimaryKey string +} + +// PageConfig defines a page to provision. +type PageConfig struct { + Slug string + ParentSlug string + Title string + TemplateKey string + Blocks []PageBlockConfig + DetailSourceType string + DetailSourceKey string + DetailSlugField string + ReconcileBlocks bool + ReconcileTemplate bool +} + +// PageBlockConfig defines a block within a provisioned page. +type PageBlockConfig struct { + BlockKey string + Title string + Content map[string]any + HtmlContent *string + Slot string + SortOrder int32 +} + +// EmbeddingConfigDef defines embedding text generation for a data table. +type EmbeddingConfigDef struct { + TableKey string + TextTemplate string + Enabled bool +} + +// MenuItemConfig defines a menu item to provision. +type MenuItemConfig struct { + Label string + URL string + PageSlug string + SortOrder int32 +} + +// EmbedConfig defines an embeddable component template. +type EmbedConfig struct { + Key string + Title string + Description string + Icon string + LabelField string + Template string + RenderFunc func(ctx context.Context, content map[string]any) string + DataSource EmbedDataSource +} + +// EmbedDataSource specifies where embed data comes from. +type EmbedDataSource struct { + Type string + TableKey string +} + +// JobScheduleConfig defines a cron schedule for a background job. +type JobScheduleConfig struct { + JobType string + CronExpression string + Config json.RawMessage +} + +// CustomColorConfig defines a custom theme color variable. +type CustomColorConfig struct { + Name string + LightValue string + DarkValue string + Source string +} diff --git a/plugin/registration.go b/plugin/registration.go new file mode 100644 index 0000000..0de11f2 --- /dev/null +++ b/plugin/registration.go @@ -0,0 +1,54 @@ +package plugin + +import ( + "context" + "io/fs" + "net/http" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/templates" +) + +// RegisterFunc is the function signature for plugin registration. +// Plugins register their templates and blocks through the provided registries. +type RegisterFunc func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error + +// PluginRegistration defines a compiled-in plugin's entry points. +type PluginRegistration struct { + Name string + + Register RegisterFunc + RegisterWithProvisioner func(tr templates.TemplateRegistry, br blocks.BlockRegistry, p Provisioner) error + + Assets func() http.Handler + Schemas func() fs.FS + SettingsSchema func() []byte + ThemePresets func() []byte + BundledFonts func() []byte + MasterPages func() []MasterPageDefinition + + HTTPHandler func(deps CoreServices) http.Handler + SettingsPanel func() string + AdminPages func() []AdminPage + + CSSManifest func() *CSSManifest + ServiceHandlers func(deps CoreServices) (*ServiceRegistration, error) + JobHandlers func(deps CoreServices) map[string]JobHandlerFunc + AIActions func() []AIAction + + DirectoryExtensions func() *DirectoryExtensions + MediaHooks MediaHooksProvider + + Load func(deps CoreServices) error + Unload func(ctx context.Context) error + + Dependencies []Dependency + Migrations func() fs.FS + Version string + + // RequiredIconPacks are icon-pack slugs the CMS must ensure are installed + // before the theme loads (e.g. "tabler", "phosphor"). Loader auto-installs + // from the bundled registry; out-of-registry slugs are logged and require + // manual install. + RequiredIconPacks []string +} diff --git a/plugin/reviews.go b/plugin/reviews.go new file mode 100644 index 0000000..4592f3e --- /dev/null +++ b/plugin/reviews.go @@ -0,0 +1,23 @@ +package plugin + +import ( + "context" + + "github.com/google/uuid" +) + +// ReviewSubmitter allows plugins to submit community reviews programmatically +// without importing CMS proto types. +type ReviewSubmitter interface { + SubmitReview(ctx context.Context, params SubmitReviewParams) (reviewID string, err error) +} + +// SubmitReviewParams contains the fields needed to submit a community review. +type SubmitReviewParams struct { + TableID uuid.UUID + RowID uuid.UUID + OverallRating int32 + ReviewText string + Ratings map[string]any + Photos []string +} diff --git a/plugin/seeder.go b/plugin/seeder.go new file mode 100644 index 0000000..a7e6c4f --- /dev/null +++ b/plugin/seeder.go @@ -0,0 +1,105 @@ +package plugin + +import ( + "context" + + "github.com/jackc/pgx/v5" +) + +// SymposiumSeeder provides Symposium data-seeding and lookup operations for +// cross-plugin use via PluginBridge. Template plugins retrieve this interface +// instead of importing the Symposium database package directly. +// +// All Seed* methods are idempotent: existing rows are returned, not duplicated. +type SymposiumSeeder interface { + // Wiki + SeedWikiCategory(ctx context.Context, tx pgx.Tx, name, slug, description string, sortOrder int32) (string, error) + SeedWikiArticle(ctx context.Context, tx pgx.Tx, title, slug, categoryID, excerpt string, contentJSON []byte) error + + // Courses + SeedCourse(ctx context.Context, tx pgx.Tx, p SeedCourseParams) (string, error) + SeedCourseSection(ctx context.Context, tx pgx.Tx, courseID, title string, position int32) (string, error) + SeedLesson(ctx context.Context, tx pgx.Tx, courseID, sectionID string, p SeedLessonParams) (string, error) + + // Quizzes + SeedQuiz(ctx context.Context, tx pgx.Tx, title string, passThreshold int32) (string, error) + SeedQuizQuestion(ctx context.Context, tx pgx.Tx, quizID string, p SeedQuizQuestionParams) (string, error) + + // Community + SeedCommunityCategory(ctx context.Context, tx pgx.Tx, name, slug, description, icon string, position, minTierLevel int32) (string, error) + + // Course categories & tags + SeedCourseCategory(ctx context.Context, tx pgx.Tx, name, slug string, description *string, position int32) error + SeedCourseTag(ctx context.Context, tx pgx.Tx, name, slug string, position *int32) error + + // Lookups — return the entity ID or an error if not found. + GetCourseBySlug(ctx context.Context, tx pgx.Tx, slug string) (string, error) + GetCommunityCategoryBySlug(ctx context.Context, tx pgx.Tx, slug string) (string, error) + + // Runtime queries + GetUserCourseProgress(ctx context.Context, pool Pool, userID string) ([]CourseProgressItem, error) +} + +// SeedCourseParams holds the parameters for seeding a course. +type SeedCourseParams struct { + Title string + Slug string + Description string + CertificateEnabled bool + MinTierLevel int32 + DifficultyLevel string + EstimatedDurationMinutes int32 +} + +// SeedLessonParams holds the parameters for seeding a lesson. +type SeedLessonParams struct { + Title string + Slug string + LessonType string + ContentJSON []byte + QuizID string + Position int32 + IsFreePreview bool + CompletionMode string +} + +// SeedQuizQuestionParams holds the parameters for seeding a quiz question. +type SeedQuizQuestionParams struct { + QuestionType string + QuestionText string + Options []map[string]any + CorrectAnswer string + Explanation string + Position int32 + Rubric string + ScenarioText string +} + +// CourseProgressItem is a lightweight view of a user's progress in one course. +type CourseProgressItem struct { + CourseTitle string + CourseSlug string + NextLesson string + DoneCount int + TotalLessons int +} + +// MessengerSeeder provides Messenger data-seeding operations for cross-plugin +// use via PluginBridge. Template plugins retrieve this interface instead of +// importing the Messenger database package directly. +type MessengerSeeder interface { + CountMessagesForPair(ctx context.Context, tx pgx.Tx, participantPairID string) (int64, error) + InsertMessage(ctx context.Context, tx pgx.Tx, p InsertMessageParams) error +} + +// InsertMessageParams holds the parameters for inserting a messenger message. +type InsertMessageParams struct { + SenderType string + SenderID string + SenderDisplayName string + RecipientType string + RecipientID string + RecipientDisplayName string + BodyMarkdown string + ParticipantPairID string +} diff --git a/plugin/service.go b/plugin/service.go new file mode 100644 index 0000000..a2430be --- /dev/null +++ b/plugin/service.go @@ -0,0 +1,61 @@ +package plugin + +import ( + "maps" + "net/http" + + "connectrpc.com/connect" + "git.dev.alexdunmow.com/block/pluginsdk/rbac" +) + +// ServiceMount is a path + handler pair for an RPC service. +type ServiceMount struct { + Path string + Handler http.Handler +} + +// ConnectServiceBinding describes a plugin RPC service mounted with the core interceptor chain. +type ConnectServiceBinding struct { + name string + build func(opts ...connect.HandlerOption) (string, http.Handler) + methodRoles map[string]rbac.Role +} + +// NewConnectServiceBinding creates a service binding whose handler is always +// constructed by core with the supplied Connect handler options. +func NewConnectServiceBinding[T any]( + name string, + svc T, + constructor func(T, ...connect.HandlerOption) (string, http.Handler), + methodRoles map[string]rbac.Role, +) ConnectServiceBinding { + clonedRoles := make(map[string]rbac.Role, len(methodRoles)) + maps.Copy(clonedRoles, methodRoles) + + return ConnectServiceBinding{ + name: name, + build: func(opts ...connect.HandlerOption) (string, http.Handler) { + return constructor(svc, opts...) + }, + methodRoles: clonedRoles, + } +} + +func (b ConnectServiceBinding) Name() string { + return b.name +} + +func (b ConnectServiceBinding) Build(opts ...connect.HandlerOption) (string, http.Handler) { + return b.build(opts...) +} + +func (b ConnectServiceBinding) MethodRoles() map[string]rbac.Role { + cloned := make(map[string]rbac.Role, len(b.methodRoles)) + maps.Copy(cloned, b.methodRoles) + return cloned +} + +// ServiceRegistration bundles plugin RPC services with their RBAC method roles. +type ServiceRegistration struct { + Services []ConnectServiceBinding +} diff --git a/plugin/topo_sort.go b/plugin/topo_sort.go new file mode 100644 index 0000000..a50e851 --- /dev/null +++ b/plugin/topo_sort.go @@ -0,0 +1,64 @@ +package plugin + +import "fmt" + +// TopologicalSort orders plugins so that dependencies come before dependents. +// Returns error on circular dependencies or missing required dependencies. +func TopologicalSort(plugins []PluginRegistration) ([]PluginRegistration, error) { + byName := make(map[string]*PluginRegistration, len(plugins)) + for i := range plugins { + byName[plugins[i].Name] = &plugins[i] + } + + for _, p := range plugins { + for _, dep := range p.Dependencies { + if dep.Required { + if _, ok := byName[dep.Plugin]; !ok { + return nil, fmt.Errorf("plugin %q requires %q, but it is not available", p.Name, dep.Plugin) + } + } + } + } + + inDegree := make(map[string]int, len(plugins)) + dependents := make(map[string][]string) + + for _, p := range plugins { + if _, ok := inDegree[p.Name]; !ok { + inDegree[p.Name] = 0 + } + for _, dep := range p.Dependencies { + if _, ok := byName[dep.Plugin]; ok { + inDegree[p.Name]++ + dependents[dep.Plugin] = append(dependents[dep.Plugin], p.Name) + } + } + } + + var queue []string + for _, p := range plugins { + if inDegree[p.Name] == 0 { + queue = append(queue, p.Name) + } + } + + var result []PluginRegistration + for len(queue) > 0 { + name := queue[0] + queue = queue[1:] + result = append(result, *byName[name]) + + for _, dep := range dependents[name] { + inDegree[dep]-- + if inDegree[dep] == 0 { + queue = append(queue, dep) + } + } + } + + if len(result) != len(plugins) { + return nil, fmt.Errorf("circular dependency detected among plugins") + } + + return result, nil +} diff --git a/plugin/types.go b/plugin/types.go new file mode 100644 index 0000000..69ae990 --- /dev/null +++ b/plugin/types.go @@ -0,0 +1,121 @@ +package plugin + +import ( + "context" + "encoding/json" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" +) + +// Pool interface for database transactions (satisfied by *pgxpool.Pool). +type Pool interface { + Begin(ctx context.Context) (pgx.Tx, error) +} + +// JobHandlerFunc is the signature for background job handlers. +type JobHandlerFunc func(ctx context.Context, config json.RawMessage, progress func(current, total int, message string)) (json.RawMessage, error) + +// Dependency declares that a plugin requires another plugin. +type Dependency struct { + Plugin string + MinVersion string + Required bool +} + +// EmailSender is the narrow plugin-facing interface for sending emails. +type EmailSender interface { + Send(to, subject, body string) error +} + +// AdminPage defines a page that a plugin registers in the admin sidebar. +type AdminPage struct { + Key string + Title string + Icon string + Route string +} + +// AIAction declares an AI task registered in the AI Action Matrix. +type AIAction struct { + Key string + Title string + Description string + DefaultProvider string + DefaultModel string +} + +// MasterPageBlock defines a block to add to a master page during provisioning. +type MasterPageBlock struct { + BlockKey string `json:"block_key"` + Title string `json:"title"` + Content map[string]any `json:"content"` + HtmlContent *string `json:"html_content"` + Slot string `json:"slot"` + SortOrder int32 `json:"sort_order"` +} + +// MasterPageDefinition defines a default master page that a plugin provisions. +type MasterPageDefinition struct { + Key string `json:"key"` + Title string `json:"title"` + PageTemplates []string `json:"page_templates"` + Blocks []MasterPageBlock `json:"blocks"` +} + +// Plugin status constants. +const ( + StatusLoaded = "loaded" + StatusFailed = "failed" + StatusDisabled = "disabled" + StatusRebuilding = "rebuilding" + StatusPendingRestart = "pending_restart" +) + +// Plugin source constants. +const ( + SourceBundled = "bundled" + SourceInstalled = "installed" +) + +// MediaAnalyzedEvent is emitted after media scanning completes. +type MediaAnalyzedEvent struct { + MediaID uuid.UUID + AnalysisID uuid.UUID + ContentHash string + Status string + SourcePlugin string + SourceType string + SourceRefID uuid.UUID + SafeAdult string + SafeViolence string + SafeRacy string +} + +// ModerationDecisionEvent is emitted when a moderation decision is made. +type ModerationDecisionEvent struct { + MediaID uuid.UUID + AnalysisID uuid.UUID + Status string + PreviousStatus string + SourcePlugin string + SourceType string + SourceRefID uuid.UUID + ModeratedBy uuid.UUID + Note string +} + +// MediaHooksProvider is the interface plugins implement to receive media lifecycle events. +type MediaHooksProvider interface { + OnMediaAnalyzed(ctx context.Context, event MediaAnalyzedEvent) error + OnModerationDecision(ctx context.Context, event ModerationDecisionEvent) error +} + +// DirectoryExtensions allows plugins to customize the directory handler UI. +type DirectoryExtensions struct { + PanelSections []func(data map[string]any) string + PinDecorators []func(pin map[string]any, data map[string]any) + BooleanFilterFields []string + SelectFilterFields []string + BadgeLabels map[string][2]string +} diff --git a/plugin/version.go b/plugin/version.go new file mode 100644 index 0000000..bda87e5 --- /dev/null +++ b/plugin/version.go @@ -0,0 +1,116 @@ +package plugin + +import ( + "bufio" + "bytes" + "fmt" + "strconv" + "strings" + + "golang.org/x/mod/semver" +) + +// ParseModVersion extracts the version string from an embedded plugin.mod file. +// Returns "0.0.0" if parsing fails or version is not found. +func ParseModVersion(data []byte) string { + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "version") { + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + val := strings.TrimSpace(parts[1]) + val = strings.Trim(val, `"`) + if val != "" { + return val + } + } + } + return "0.0.0" +} + +// ParseRequiredIconPacks extracts the required_icon_packs list from an embedded +// plugin.mod file. Returns nil if the field is absent or empty. Mirrors the +// "read straight from TOML bytes" style of ParseModVersion so plugin +// registration.go files can populate PluginRegistration.RequiredIconPacks +// without having to duplicate the slugs in Go. +func ParseRequiredIconPacks(data []byte) []string { + m, err := ParseModFull(data) + if err != nil || m == nil { + return nil + } + if len(m.Plugin.RequiredIconPacks) == 0 { + return nil + } + out := make([]string, 0, len(m.Plugin.RequiredIconPacks)) + for _, slug := range m.Plugin.RequiredIconPacks { + s := strings.TrimSpace(slug) + if s == "" { + continue + } + out = append(out, s) + } + if len(out) == 0 { + return nil + } + return out +} + +// CompareVersions compares two semver strings. +// Returns -1 if v1 < v2, 0 if equal, +1 if v1 > v2. +// Returns 0 if either version is invalid. +func CompareVersions(v1, v2 string) int { + if !strings.HasPrefix(v1, "v") { + v1 = "v" + v1 + } + if !strings.HasPrefix(v2, "v") { + v2 = "v" + v2 + } + if !semver.IsValid(v1) || !semver.IsValid(v2) { + return 0 + } + return semver.Compare(v1, v2) +} + +// ParseBaseSemver parses a plain MAJOR.MINOR.PATCH version into integers. +// Rejects pre-release suffixes and build metadata. +func ParseBaseSemver(s string) (major, minor, patch int, err error) { + parts := strings.Split(s, ".") + if len(parts) != 3 { + return 0, 0, 0, fmt.Errorf("invalid version %q: expected MAJOR.MINOR.PATCH", s) + } + out := [3]int{} + for i, p := range parts { + n, perr := strconv.Atoi(p) + if perr != nil || n < 0 { + return 0, 0, 0, fmt.Errorf("invalid version %q: each part must be a non-negative integer", s) + } + out[i] = n + } + return out[0], out[1], out[2], nil +} + +// BumpVersion returns current bumped at the given level ("major", "minor", or "patch"). +// Bumping major resets minor and patch to 0; bumping minor resets patch to 0. +func BumpVersion(current, level string) (string, error) { + major, minor, patch, err := ParseBaseSemver(current) + if err != nil { + return "", err + } + switch level { + case "major": + major++ + minor = 0 + patch = 0 + case "minor": + minor++ + patch = 0 + case "patch": + patch++ + default: + return "", fmt.Errorf("unknown bump level %q (want major|minor|patch)", level) + } + return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil +} diff --git a/plugin/version_test.go b/plugin/version_test.go new file mode 100644 index 0000000..b2790d3 --- /dev/null +++ b/plugin/version_test.go @@ -0,0 +1,77 @@ +package plugin + +import "testing" + +func TestParseBaseSemver(t *testing.T) { + cases := []struct { + in string + major, minor, patch int + wantErr bool + }{ + {"0.1.0", 0, 1, 0, false}, + {"1.0.0", 1, 0, 0, false}, + {"12.34.567", 12, 34, 567, false}, + {"0.0.0", 0, 0, 0, false}, + {"v0.1.0", 0, 0, 0, true}, + {"0.1", 0, 0, 0, true}, + {"0.1.0.0", 0, 0, 0, true}, + {"0.1.0-beta", 0, 0, 0, true}, + {"0.1.0+build", 0, 0, 0, true}, + {"-1.0.0", 0, 0, 0, true}, + {"a.b.c", 0, 0, 0, true}, + {"", 0, 0, 0, true}, + } + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + maj, min, pat, err := ParseBaseSemver(c.in) + if c.wantErr { + if err == nil { + t.Fatalf("ParseBaseSemver(%q): expected error, got %d.%d.%d", c.in, maj, min, pat) + } + return + } + if err != nil { + t.Fatalf("ParseBaseSemver(%q): unexpected error: %v", c.in, err) + } + if maj != c.major || min != c.minor || pat != c.patch { + t.Errorf("ParseBaseSemver(%q) = %d.%d.%d, want %d.%d.%d", c.in, maj, min, pat, c.major, c.minor, c.patch) + } + }) + } +} + +func TestBumpVersion(t *testing.T) { + cases := []struct { + current, level, want string + wantErr bool + }{ + {"0.1.0", "patch", "0.1.1", false}, + {"0.1.0", "minor", "0.2.0", false}, + {"0.1.0", "major", "1.0.0", false}, + {"1.2.3", "patch", "1.2.4", false}, + {"1.2.3", "minor", "1.3.0", false}, + {"1.2.3", "major", "2.0.0", false}, + {"0.0.0", "patch", "0.0.1", false}, + {"0.1.0", "build", "", true}, + {"0.1.0", "", "", true}, + {"v0.1.0", "patch", "", true}, + {"", "patch", "", true}, + } + for _, c := range cases { + t.Run(c.current+"/"+c.level, func(t *testing.T) { + got, err := BumpVersion(c.current, c.level) + if c.wantErr { + if err == nil { + t.Fatalf("BumpVersion(%q, %q): expected error, got %q", c.current, c.level, got) + } + return + } + if err != nil { + t.Fatalf("BumpVersion(%q, %q): unexpected error: %v", c.current, c.level, err) + } + if got != c.want { + t.Errorf("BumpVersion(%q, %q) = %q, want %q", c.current, c.level, got, c.want) + } + }) + } +} diff --git a/plugin/wasmguest/bnwasm/dbvalue.go b/plugin/wasmguest/bnwasm/dbvalue.go new file mode 100644 index 0000000..10957b5 --- /dev/null +++ b/plugin/wasmguest/bnwasm/dbvalue.go @@ -0,0 +1,324 @@ +package bnwasm + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// toDbValues marshals a positional argument list to wire DbValues. Named args +// (pgx.NamedArgs, or any pgx.QueryRewriter passed as the sole argument) are +// rejected: sqlc emits positional parameters, and rewriting a query host-side +// is out of scope for v1. +func toDbValues(args []any) ([]*abiv1.DbValue, error) { + if len(args) == 1 { + if _, ok := args[0].(pgx.QueryRewriter); ok { + return nil, fmt.Errorf("bnwasm: named arguments (pgx.QueryRewriter/NamedArgs) are not supported; use positional parameters") + } + } + out := make([]*abiv1.DbValue, len(args)) + for i, a := range args { + v, err := toDbValue(a) + if err != nil { + return nil, fmt.Errorf("bnwasm: arg %d: %w", i, err) + } + out[i] = v + } + return out, nil +} + +// toDbValue maps one Go query argument to a DbValue. Concrete BlockNinja/pgx +// types (uuid, time, text[], json) are special-cased so they round-trip to +// their dedicated DbValue variant; anything implementing driver.Valuer (pgtype +// scalars, sql.Null*) is normalized through Value(); the rest goes by kind. +func toDbValue(a any) (*abiv1.DbValue, error) { + switch v := a.(type) { + case nil: + return nullValue(), nil + case uuid.UUID: + return &abiv1.DbValue{Kind: &abiv1.DbValue_UuidValue{UuidValue: v.String()}}, nil + case uuid.NullUUID: + if !v.Valid { + return nullValue(), nil + } + return &abiv1.DbValue{Kind: &abiv1.DbValue_UuidValue{UuidValue: v.UUID.String()}}, nil + case time.Time: + return &abiv1.DbValue{Kind: &abiv1.DbValue_TimestampValue{TimestampValue: timestamppb.New(v)}}, nil + case json.RawMessage: + if v == nil { + return nullValue(), nil + } + return &abiv1.DbValue{Kind: &abiv1.DbValue_JsonbValue{JsonbValue: v}}, nil + case []byte: + if v == nil { + return nullValue(), nil + } + return &abiv1.DbValue{Kind: &abiv1.DbValue_BytesValue{BytesValue: v}}, nil + case []string: + // nil → NULL (matching []byte/json.RawMessage); an empty-but-non-nil + // slice stays a non-NULL empty text[]. + if v == nil { + return nullValue(), nil + } + return &abiv1.DbValue{Kind: &abiv1.DbValue_TextArrayValue{TextArrayValue: &abiv1.TextArray{Values: v}}}, nil + case []uuid.UUID: + // uuid[] gets its own variant so the host binds a native uuid[] + // parameter (queries keep `ANY($1::uuid[])` — no text[]-cast + // workaround). nil → NULL / empty stays a non-NULL empty uuid[], + // mirroring the []string convention. + if v == nil { + return nullValue(), nil + } + strs := make([]string, len(v)) + for i, id := range v { + strs[i] = id.String() + } + return &abiv1.DbValue{Kind: &abiv1.DbValue_UuidArrayValue{UuidArrayValue: &abiv1.UuidArray{Values: strs}}}, nil + case string: + return &abiv1.DbValue{Kind: &abiv1.DbValue_StringValue{StringValue: v}}, nil + case bool: + return &abiv1.DbValue{Kind: &abiv1.DbValue_BoolValue{BoolValue: v}}, nil + } + + // driver.Valuer covers pgtype scalars (Timestamptz, Numeric, Int4, ...) and + // sql.Null*; recurse on the normalized driver.Value. + if valuer, ok := a.(driver.Valuer); ok { + dv, err := valuer.Value() + if err != nil { + return nil, err + } + if dv == nil { + return nullValue(), nil + } + return toDbValue(dv) + } + + // Fall back to reflection for pointers and the basic numeric kinds. + rv := reflect.ValueOf(a) + switch rv.Kind() { + case reflect.Pointer: + if rv.IsNil() { + return nullValue(), nil + } + return toDbValue(rv.Elem().Interface()) + case reflect.Bool: + return &abiv1.DbValue{Kind: &abiv1.DbValue_BoolValue{BoolValue: rv.Bool()}}, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return &abiv1.DbValue{Kind: &abiv1.DbValue_Int64Value{Int64Value: rv.Int()}}, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return &abiv1.DbValue{Kind: &abiv1.DbValue_Int64Value{Int64Value: int64(rv.Uint())}}, nil + case reflect.Float32, reflect.Float64: + return &abiv1.DbValue{Kind: &abiv1.DbValue_Float64Value{Float64Value: rv.Float()}}, nil + case reflect.String: + return &abiv1.DbValue{Kind: &abiv1.DbValue_StringValue{StringValue: rv.String()}}, nil + } + return nil, fmt.Errorf("bnwasm: unsupported argument type %T", a) +} + +func nullValue() *abiv1.DbValue { + return &abiv1.DbValue{Kind: &abiv1.DbValue_Null{Null: true}} +} + +// naturalValue returns the canonical Go scan value for a DbValue and whether it +// is SQL NULL. This is the semantic contract WO-WZ-007 (host executor) mirrors: +// +// null → (nil, true) +// bool → (bool, false) +// int64 → (int64, false) +// float64 → (float64, false) +// string → (string, false) +// bytes → ([]byte, false) +// timestamp → (time.Time,false) +// uuid → (string, false) // canonical form; uuid.UUID.Scan accepts it +// jsonb → ([]byte, false) +// numeric → (string, false) // lossless decimal string +// text_array → ([]string, false) +// uuid_array → ([]string, false) // canonical forms; assign parses into []uuid.UUID +func naturalValue(dv *abiv1.DbValue) (any, bool) { + switch k := dv.GetKind().(type) { + case *abiv1.DbValue_Null: + return nil, true + case *abiv1.DbValue_BoolValue: + return k.BoolValue, false + case *abiv1.DbValue_Int64Value: + return k.Int64Value, false + case *abiv1.DbValue_Float64Value: + return k.Float64Value, false + case *abiv1.DbValue_StringValue: + return k.StringValue, false + case *abiv1.DbValue_BytesValue: + return k.BytesValue, false + case *abiv1.DbValue_TimestampValue: + return k.TimestampValue.AsTime(), false + case *abiv1.DbValue_UuidValue: + return k.UuidValue, false + case *abiv1.DbValue_JsonbValue: + return []byte(k.JsonbValue), false + case *abiv1.DbValue_NumericValue: + return k.NumericValue, false + case *abiv1.DbValue_TextArrayValue: + return append([]string(nil), k.TextArrayValue.GetValues()...), false + case *abiv1.DbValue_UuidArrayValue: + // Raw canonical strings, mirroring the single-uuid arm returning a + // string: assign() parses them into a []uuid.UUID destination (with + // error propagation), the same way uuid.UUID.Scan parses the scalar. + return append([]string(nil), k.UuidArrayValue.GetValues()...), false + default: + return nil, true + } +} + +// uuidSliceType is the reflect.Type of []uuid.UUID, the canonical scan +// destination sqlc emits for a uuid[] column (and COALESCE(...::uuid[]) arg). +var uuidSliceType = reflect.TypeFor[[]uuid.UUID]() + +// scanValue assigns a DbValue into a destination pointer with pgx-flavored Scan +// semantics: a nil dest skips the column; a sql.Scanner destination is fed the +// natural value (including nil for NULL); pointer-to-pointer destinations model +// nullability (set nil on NULL); everything else is assigned by reflection with +// integer/float/string/[]byte/[]string coercion. +func scanValue(dv *abiv1.DbValue, dest any) error { + if dest == nil { + return nil // pgx: nil dest skips the value entirely + } + nv, isNull := naturalValue(dv) + + if sc, ok := dest.(sql.Scanner); ok { + return sc.Scan(nv) + } + + rv := reflect.ValueOf(dest) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return fmt.Errorf("bnwasm: scan destination must be a non-nil pointer, got %T", dest) + } + elem := rv.Elem() + + // Pointer-to-pointer (e.g. **string): nullable column mapped to *string. + if elem.Kind() == reflect.Pointer { + if isNull { + elem.Set(reflect.Zero(elem.Type())) + return nil + } + np := reflect.New(elem.Type().Elem()) + if err := assign(np.Elem(), nv); err != nil { + return err + } + elem.Set(np) + return nil + } + + if isNull { + elem.Set(reflect.Zero(elem.Type())) + return nil + } + return assign(elem, nv) +} + +// assign coerces a natural value into a concrete (non-pointer) destination. +func assign(dst reflect.Value, nv any) error { + src := reflect.ValueOf(nv) + + // uuid[] natural value ([]string of canonical UUIDs) → []uuid.UUID: parse + // each element (mirrors uuid.UUID.Scan for the scalar). reflect can neither + // assign nor convert []string to []uuid.UUID, so this must be explicit. + if dst.Type() == uuidSliceType { + if strs, ok := nv.([]string); ok { + out := make([]uuid.UUID, len(strs)) + for i, s := range strs { + id, err := uuid.Parse(s) + if err != nil { + return fmt.Errorf("bnwasm: cannot scan %q into uuid at index %d: %w", s, i, err) + } + out[i] = id + } + dst.Set(reflect.ValueOf(out)) + return nil + } + } + + // Direct assignability (string→string, time.Time→time.Time, []string→[]string). + if src.Type().AssignableTo(dst.Type()) { + dst.Set(src) + return nil + } + // Convertibility (json.RawMessage←[]byte, int64→int32, float64→float32, + // named string types, []byte→string, ...). + if src.Type().ConvertibleTo(dst.Type()) { + // Guard against lossy string<->numeric "conversions" reflect allows for + // rune/byte-ish types by only converting between compatible kinds. + if convertibleKinds(src.Kind(), dst.Kind()) { + dst.Set(src.Convert(dst.Type())) + return nil + } + } + return fmt.Errorf("bnwasm: cannot scan %s into %s", src.Type(), dst.Type()) +} + +func convertibleKinds(src, dst reflect.Kind) bool { + num := func(k reflect.Kind) bool { + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + } + return false + } + switch { + case num(src) && num(dst): + return true + case src == reflect.String && dst == reflect.String: + return true + case src == reflect.Slice && dst == reflect.Slice: // []byte→json.RawMessage, []string→named + return true + case src == reflect.Slice && dst == reflect.String: // []byte→string + return true + case src == reflect.String && dst == reflect.Slice: // string→[]byte + return true + } + return false +} + +// driverValue maps a DbValue to a database/sql driver.Value (the closed set: +// nil, int64, float64, bool, []byte, string, time.Time). uuid/numeric surface +// as strings and text[] as a Postgres array literal string, since driver.Value +// has no richer representation; callers scan those through the column's +// sql.Scanner as usual. +func driverValue(dv *abiv1.DbValue) driver.Value { + nv, isNull := naturalValue(dv) + if isNull { + return nil + } + switch v := nv.(type) { + case []string: + return encodePgTextArray(v) + default: + return v + } +} + +// encodePgTextArray renders a []string as a Postgres text[] literal, e.g. +// {"a","b,c"}, quoting/escaping every element for lossless reconstruction. +func encodePgTextArray(vals []string) string { + var b strings.Builder + b.WriteByte('{') + for i, s := range vals { + if i > 0 { + b.WriteByte(',') + } + b.WriteByte('"') + b.WriteString(strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s)) + b.WriteByte('"') + } + b.WriteByte('}') + return b.String() +} diff --git a/plugin/wasmguest/bnwasm/dbvalue_fixtures.go b/plugin/wasmguest/bnwasm/dbvalue_fixtures.go new file mode 100644 index 0000000..c229f30 --- /dev/null +++ b/plugin/wasmguest/bnwasm/dbvalue_fixtures.go @@ -0,0 +1,203 @@ +package bnwasm + +import ( + "encoding/json" + "time" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// FixtureTime is the fixed timestamp used by the shared DbValue fixtures, so +// both sides of the boundary compare against one deterministic value. +var FixtureTime = time.Date(2026, 7, 3, 12, 30, 0, 0, time.UTC) + +// FixtureUUID is the fixed UUID used by the shared DbValue fixtures. +var FixtureUUID = uuid.MustParse("11111111-2222-3333-4444-555555555555") + +// FixtureUUIDs are the fixed, ORDERED UUIDs used by the uuid_array fixture; the +// round-trip must preserve both their values and their order. +var FixtureUUIDs = []uuid.UUID{ + uuid.MustParse("aaaaaaaa-0000-0000-0000-000000000001"), + uuid.MustParse("bbbbbbbb-0000-0000-0000-000000000002"), + uuid.MustParse("cccccccc-0000-0000-0000-000000000003"), +} + +// DbValueFixture is one entry in the canonical DbValue↔Go mapping table. +type DbValueFixture struct { + // Name identifies the DbValue variant. + Name string + // Value is the wire value the host would send back in a DbRowsResponse cell + // (and the guest would send as an argument for the non-NULL scalar cases). + Value *abiv1.DbValue + // NewDest returns a fresh pointer to the canonical Go scan destination type + // that plugin sqlc code binds for this variant. + NewDest func() any + // Want is the expected dereferenced value after scanning Value into + // NewDest(). Compared with reflect.DeepEqual (times via .Equal). + Want any + // DriverValue is the expected database/sql driver.Value for this variant, + // documenting the "bnwasm" driver's mapping (uuid/numeric→string, + // text[]→Postgres array literal). + DriverValue any +} + +// DbValueFixtures is the authoritative DbValue↔Go mapping table. It is the +// shared contract WO-WZ-007 (the CMS host executor) MUST mirror: every variant +// the guest driver produces on scan, the host must be able to build on read, +// and vice-versa. Exported (non-test) precisely so the host's tests in the CMS +// module can import and assert against the same table rather than duplicating a +// drift-prone copy. +// +// Every oneof arm of abiv1.DbValue.Kind appears at least once; several arms +// carry extra entries that exercise encoding edge cases (the zero timestamp, +// negative/very-large numerics, an empty text[], and text[] elements that force +// encodePgTextArray's quoting/escaping paths) so the host mirror must reproduce +// them too. +// +// Contract limit — NULL array elements: abiv1.TextArray is a repeated string, +// which has no per-element NULL. A Postgres text[] value like '{a,NULL,b}' +// therefore CANNOT round-trip through this boundary: a NULL element collapses to +// the empty string "". The whole array can still be SQL NULL (a nil []string / +// DbValue_Null), but an individual NULL *inside* the array is unrepresentable. +// The WO-WZ-007 host executor MUST honor this same limit (encode a NULL element +// as "" or reject it) — it must not invent a sentinel. +var DbValueFixtures = []DbValueFixture{ + { + Name: "null", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Null{Null: true}}, + NewDest: func() any { return new(*string) }, // **string: NULL → (*string)(nil) + Want: (*string)(nil), + DriverValue: nil, + }, + { + Name: "bool", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_BoolValue{BoolValue: true}}, + NewDest: func() any { return new(bool) }, + Want: true, + DriverValue: true, + }, + { + Name: "int64", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Int64Value{Int64Value: 42}}, + NewDest: func() any { return new(int64) }, + Want: int64(42), + DriverValue: int64(42), + }, + { + Name: "int32", // narrowing coercion sqlc emits for int4 columns + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Int64Value{Int64Value: 7}}, + NewDest: func() any { return new(int32) }, + Want: int32(7), + DriverValue: int64(7), + }, + { + Name: "float64", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_Float64Value{Float64Value: 3.5}}, + NewDest: func() any { return new(float64) }, + Want: 3.5, + DriverValue: 3.5, + }, + { + Name: "string", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_StringValue{StringValue: "hello"}}, + NewDest: func() any { return new(string) }, + Want: "hello", + DriverValue: "hello", + }, + { + Name: "bytes", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_BytesValue{BytesValue: []byte{0x01, 0x02, 0x03}}}, + NewDest: func() any { return new([]byte) }, + Want: []byte{0x01, 0x02, 0x03}, + DriverValue: []byte{0x01, 0x02, 0x03}, + }, + { + Name: "timestamp", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TimestampValue{TimestampValue: timestamppb.New(FixtureTime)}}, + NewDest: func() any { return new(time.Time) }, + Want: FixtureTime, + DriverValue: FixtureTime, + }, + { + Name: "uuid", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_UuidValue{UuidValue: FixtureUUID.String()}}, + NewDest: func() any { return new(uuid.UUID) }, + Want: FixtureUUID, + DriverValue: FixtureUUID.String(), + }, + { + Name: "jsonb", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_JsonbValue{JsonbValue: []byte(`{"a":1}`)}}, + NewDest: func() any { return new(json.RawMessage) }, + Want: json.RawMessage(`{"a":1}`), + DriverValue: []byte(`{"a":1}`), + }, + { + Name: "numeric", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_NumericValue{NumericValue: "123.45"}}, + NewDest: func() any { return new(string) }, + Want: "123.45", + DriverValue: "123.45", + }, + { + Name: "text_array", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TextArrayValue{TextArrayValue: &abiv1.TextArray{Values: []string{"a", "b"}}}}, + NewDest: func() any { return new([]string) }, + Want: []string{"a", "b"}, + DriverValue: `{"a","b"}`, + }, + { + // uuid[]: three ordered UUIDs bound as a native uuid[] parameter and + // scanned back into []uuid.UUID. Ordering is part of the contract — the + // host mirror (dbexec.go) must preserve element order. + Name: "uuid_array", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_UuidArrayValue{UuidArrayValue: &abiv1.UuidArray{Values: []string{ + FixtureUUIDs[0].String(), FixtureUUIDs[1].String(), FixtureUUIDs[2].String(), + }}}}, + NewDest: func() any { return new([]uuid.UUID) }, + Want: FixtureUUIDs, + DriverValue: `{"aaaaaaaa-0000-0000-0000-000000000001","bbbbbbbb-0000-0000-0000-000000000002","cccccccc-0000-0000-0000-000000000003"}`, + }, + + // --- encoding edge cases (extra entries beyond one-per-variant) --- + { + Name: "timestamp_zero", // the Go zero time.Time (year 1) must survive the timestamppb round-trip + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TimestampValue{TimestampValue: timestamppb.New(time.Time{})}}, + NewDest: func() any { return new(time.Time) }, + Want: time.Time{}, + DriverValue: time.Time{}, + }, + { + Name: "numeric_negative", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_NumericValue{NumericValue: "-98765.4321"}}, + NewDest: func() any { return new(string) }, + Want: "-98765.4321", + DriverValue: "-98765.4321", + }, + { + Name: "numeric_large", // far beyond int64/float64 range: numeric stays a lossless decimal string + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_NumericValue{NumericValue: "123456789012345678901234567890.123456789"}}, + NewDest: func() any { return new(string) }, + Want: "123456789012345678901234567890.123456789", + DriverValue: "123456789012345678901234567890.123456789", + }, + { + Name: "text_array_empty", // empty-but-non-nil text[] → "{}" (distinct from a NULL array) + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TextArrayValue{TextArrayValue: &abiv1.TextArray{Values: []string{}}}}, + NewDest: func() any { return new([]string) }, + Want: []string(nil), + DriverValue: "{}", + }, + { + // Elements that force encodePgTextArray's quoting/escaping: a comma + // (needs quoting), a double-quote and a backslash (need escaping), and an + // empty-string element (renders as the empty quoted ""). + Name: "text_array_quoting", + Value: &abiv1.DbValue{Kind: &abiv1.DbValue_TextArrayValue{TextArrayValue: &abiv1.TextArray{Values: []string{"a,b", `x"y`, `p\q`, ""}}}}, + NewDest: func() any { return new([]string) }, + Want: []string{"a,b", `x"y`, `p\q`, ""}, + DriverValue: `{"a,b","x\"y","p\\q",""}`, + }, +} diff --git a/plugin/wasmguest/bnwasm/dbvalue_test.go b/plugin/wasmguest/bnwasm/dbvalue_test.go new file mode 100644 index 0000000..8ea0358 --- /dev/null +++ b/plugin/wasmguest/bnwasm/dbvalue_test.go @@ -0,0 +1,119 @@ +package bnwasm + +import ( + "encoding/json" + "reflect" + "testing" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/google/uuid" +) + +// TestToDbValueTextArrayNilVsEmpty pins the guest-side arg encoding for []string: +// a nil slice marshals to SQL NULL (matching []byte/json.RawMessage), while an +// empty-but-non-nil slice stays a non-NULL empty text[]. WO-WZ-007's host must +// mirror this nil→NULL convention. +func TestToDbValueTextArrayNilVsEmpty(t *testing.T) { + t.Run("nil", func(t *testing.T) { + dv, err := toDbValue([]string(nil)) + if err != nil { + t.Fatal(err) + } + if _, ok := dv.GetKind().(*abiv1.DbValue_Null); !ok { + t.Fatalf("nil []string: want DbValue_Null, got %T", dv.GetKind()) + } + }) + t.Run("empty", func(t *testing.T) { + dv, err := toDbValue([]string{}) + if err != nil { + t.Fatal(err) + } + ta, ok := dv.GetKind().(*abiv1.DbValue_TextArrayValue) + if !ok { + t.Fatalf("empty []string: want DbValue_TextArrayValue, got %T", dv.GetKind()) + } + if got := ta.TextArrayValue.GetValues(); len(got) != 0 { + t.Fatalf("empty []string: want zero-length text[], got %#v", got) + } + }) + t.Run("values", func(t *testing.T) { + dv, err := toDbValue([]string{"a", "b"}) + if err != nil { + t.Fatal(err) + } + ta, ok := dv.GetKind().(*abiv1.DbValue_TextArrayValue) + if !ok { + t.Fatalf("want DbValue_TextArrayValue, got %T", dv.GetKind()) + } + if got := ta.TextArrayValue.GetValues(); !reflect.DeepEqual(got, []string{"a", "b"}) { + t.Fatalf("want [a b], got %#v", got) + } + }) +} + +// TestToDbValueUuidArray pins the guest-side arg encoding for []uuid.UUID: a nil +// slice marshals to SQL NULL, an empty-but-non-nil slice stays a non-NULL empty +// uuid[], and values marshal to canonical strings in order. The host binds this +// as a native uuid[] parameter (no text[]-cast workaround). +func TestToDbValueUuidArray(t *testing.T) { + t.Run("nil", func(t *testing.T) { + dv, err := toDbValue([]uuid.UUID(nil)) + if err != nil { + t.Fatal(err) + } + if _, ok := dv.GetKind().(*abiv1.DbValue_Null); !ok { + t.Fatalf("nil []uuid.UUID: want DbValue_Null, got %T", dv.GetKind()) + } + }) + t.Run("empty", func(t *testing.T) { + dv, err := toDbValue([]uuid.UUID{}) + if err != nil { + t.Fatal(err) + } + ua, ok := dv.GetKind().(*abiv1.DbValue_UuidArrayValue) + if !ok { + t.Fatalf("empty []uuid.UUID: want DbValue_UuidArrayValue, got %T", dv.GetKind()) + } + if got := ua.UuidArrayValue.GetValues(); len(got) != 0 { + t.Fatalf("empty []uuid.UUID: want zero-length uuid[], got %#v", got) + } + }) + t.Run("values", func(t *testing.T) { + dv, err := toDbValue(FixtureUUIDs) + if err != nil { + t.Fatal(err) + } + ua, ok := dv.GetKind().(*abiv1.DbValue_UuidArrayValue) + if !ok { + t.Fatalf("want DbValue_UuidArrayValue, got %T", dv.GetKind()) + } + want := []string{FixtureUUIDs[0].String(), FixtureUUIDs[1].String(), FixtureUUIDs[2].String()} + if got := ua.UuidArrayValue.GetValues(); !reflect.DeepEqual(got, want) { + t.Fatalf("want %v, got %#v", want, got) + } + }) +} + +// TestToDbValueNilConvention locks the nil→NULL convention across the reference +// types so []string stays consistent with []byte and json.RawMessage. +func TestToDbValueNilConvention(t *testing.T) { + cases := []struct { + name string + in any + }{ + {"bytes", []byte(nil)}, + {"json", json.RawMessage(nil)}, + {"text_array", []string(nil)}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + dv, err := toDbValue(c.in) + if err != nil { + t.Fatal(err) + } + if _, ok := dv.GetKind().(*abiv1.DbValue_Null); !ok { + t.Fatalf("nil %s: want DbValue_Null, got %T", c.name, dv.GetKind()) + } + }) + } +} diff --git a/plugin/wasmguest/bnwasm/driver.go b/plugin/wasmguest/bnwasm/driver.go new file mode 100644 index 0000000..95a3112 --- /dev/null +++ b/plugin/wasmguest/bnwasm/driver.go @@ -0,0 +1,201 @@ +package bnwasm + +import ( + "context" + "database/sql" + "database/sql/driver" + "fmt" + "io" + "sync" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" +) + +// DriverName is the database/sql driver name registered by this package. +const DriverName = "bnwasm" + +func init() { sql.Register(DriverName, Driver{}) } + +// defaultTransport is the process-wide transport the "bnwasm" database/sql +// driver uses (the guest has exactly one host). The wasip1 shim sets it via +// SetDefaultTransport; native builds leave it nil so opened connections fail +// cleanly with errNoHost. Guarded because sql.DB may dial from any goroutine. +var ( + defaultMu sync.RWMutex + defaultTransport Transport +) + +// SetDefaultTransport binds the transport used by database/sql connections +// opened via sql.Open("bnwasm", ...). Called once from the wasm guest shim. +func SetDefaultTransport(t Transport) { + defaultMu.Lock() + defaultTransport = t + defaultMu.Unlock() +} + +func currentTransport() Transport { + defaultMu.RLock() + defer defaultMu.RUnlock() + return defaultTransport +} + +// Driver is the database/sql/driver.Driver for the wasm DB ABI. +type Driver struct{} + +var ( + _ driver.Driver = Driver{} + _ driver.DriverContext = Driver{} +) + +// Open ignores its DSN — the transport is process-global (one host per guest). +func (d Driver) Open(name string) (driver.Conn, error) { + return &conn{t: currentTransport()}, nil +} + +// OpenConnector lets sql.OpenDB bypass DSN parsing entirely. +func (d Driver) OpenConnector(name string) (driver.Connector, error) { + return connector{}, nil +} + +// NewConnector returns a driver.Connector bound to an explicit transport, for +// callers that construct a *sql.DB via sql.OpenDB without touching the global. +func NewConnector(t Transport) driver.Connector { return connector{t: t, explicit: true} } + +type connector struct { + t Transport + explicit bool +} + +func (c connector) Connect(context.Context) (driver.Conn, error) { + t := c.t + if !c.explicit { + t = currentTransport() + } + return &conn{t: t}, nil +} + +func (c connector) Driver() driver.Driver { return Driver{} } + +// conn is one logical connection: it holds no real connection state guest-side, +// only the transport and the current transaction handle (0 = autocommit). +type conn struct { + t Transport + txHandle uint64 + inTx bool +} + +var ( + _ driver.Conn = (*conn)(nil) + _ driver.QueryerContext = (*conn)(nil) + _ driver.ExecerContext = (*conn)(nil) + _ driver.ConnBeginTx = (*conn)(nil) +) + +func (c *conn) Prepare(query string) (driver.Stmt, error) { + return nil, fmt.Errorf("bnwasm: prepared statements are not supported; use QueryContext/ExecContext") +} + +func (c *conn) Close() error { return nil } + +func (c *conn) Begin() (driver.Tx, error) { return c.BeginTx(context.Background(), driver.TxOptions{}) } + +func (c *conn) BeginTx(ctx context.Context, _ driver.TxOptions) (driver.Tx, error) { + if c.inTx { + return nil, errNestedTx + } + handle, err := c.t.txBegin(ctx) + if err != nil { + return nil, err + } + c.txHandle = handle + c.inTx = true + return &sqlTx{c: c}, nil +} + +func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + posArgs, err := namedToPositional(args) + if err != nil { + return nil, err + } + tag, err := c.t.exec(ctx, query, c.txHandle, posArgs) + if err != nil { + return nil, err + } + return sqlResult{rowsAffected: tag.RowsAffected()}, nil +} + +func (c *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + posArgs, err := namedToPositional(args) + if err != nil { + return nil, err + } + resp, err := c.t.query(ctx, query, c.txHandle, posArgs) + if err != nil { + return nil, err + } + return newDriverRows(resp), nil +} + +// sqlTx is the database/sql transaction handle; it clears the conn's handle on +// completion so subsequent statements run in autocommit again. +type sqlTx struct{ c *conn } + +func (t sqlTx) Commit() error { + err := t.c.t.txCommit(context.Background(), t.c.txHandle) + t.c.txHandle, t.c.inTx = 0, false + return err +} + +func (t sqlTx) Rollback() error { + err := t.c.t.txRollback(context.Background(), t.c.txHandle) + t.c.txHandle, t.c.inTx = 0, false + return err +} + +type sqlResult struct{ rowsAffected int64 } + +func (r sqlResult) LastInsertId() (int64, error) { + return 0, fmt.Errorf("bnwasm: LastInsertId is not supported (Postgres uses RETURNING)") +} +func (r sqlResult) RowsAffected() (int64, error) { return r.rowsAffected, nil } + +// driverRows adapts a buffered DbRowsResponse to database/sql/driver.Rows. +type driverRows struct { + resp *abiv1.DbRowsResponse + idx int +} + +func newDriverRows(resp *abiv1.DbRowsResponse) *driverRows { return &driverRows{resp: resp, idx: 0} } + +func (r *driverRows) Columns() []string { return r.resp.GetColumns() } + +func (r *driverRows) Close() error { return nil } + +func (r *driverRows) Next(dest []driver.Value) error { + rows := r.resp.GetRows() + if r.idx >= len(rows) { + return io.EOF + } + cells := rows[r.idx].GetValues() + if len(dest) != len(cells) { + return fmt.Errorf("bnwasm: expected %d columns, got %d destinations", len(cells), len(dest)) + } + for i, cell := range cells { + dest[i] = driverValue(cell) + } + r.idx++ + return nil +} + +// namedToPositional flattens database/sql NamedValues to positional args, +// rejecting any that carry a name (sqlc emits positional $N parameters only). +func namedToPositional(args []driver.NamedValue) ([]any, error) { + out := make([]any, len(args)) + for i, a := range args { + if a.Name != "" { + return nil, fmt.Errorf("bnwasm: named argument %q is not supported; use positional parameters", a.Name) + } + out[i] = a.Value + } + return out, nil +} diff --git a/plugin/wasmguest/bnwasm/driver_test.go b/plugin/wasmguest/bnwasm/driver_test.go new file mode 100644 index 0000000..1e8d2e2 --- /dev/null +++ b/plugin/wasmguest/bnwasm/driver_test.go @@ -0,0 +1,300 @@ +package bnwasm + +import ( + "context" + "database/sql" + "errors" + "reflect" + "slices" + "testing" + "time" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "google.golang.org/protobuf/proto" +) + +// recordedCall captures one db.* host call for ordered-sequence assertions. +type recordedCall struct { + method string + sql string + args []*abiv1.DbValue + txHandle uint64 +} + +// fakeHost is a scripted db.* transport: it records every call and answers from +// programmable fields, so the guest driver is exercised end-to-end with no real +// Postgres and the exact host-call sequence can be asserted. +type fakeHost struct { + calls []recordedCall + + queryResp *abiv1.DbRowsResponse // returned for db.query + execRows int64 // rows_affected for db.exec + txHandle uint64 // handle handed out on db.tx_begin + dbError *abiv1.DbError // if set, attached to the next query/exec response + failWith error // if set, the transport returns this transport-level error +} + +func (f *fakeHost) call(method string, req, resp proto.Message) error { + rc := recordedCall{method: method} + switch r := req.(type) { + case *abiv1.DbQueryRequest: + rc.sql, rc.args, rc.txHandle = r.GetSql(), r.GetArgs(), r.GetTxHandle() + case *abiv1.DbExecRequest: + rc.sql, rc.args, rc.txHandle = r.GetSql(), r.GetArgs(), r.GetTxHandle() + case *abiv1.DbTxCommitRequest: + rc.txHandle = r.GetTxHandle() + case *abiv1.DbTxRollbackRequest: + rc.txHandle = r.GetTxHandle() + } + f.calls = append(f.calls, rc) + + if f.failWith != nil { + return f.failWith + } + switch out := resp.(type) { + case *abiv1.DbRowsResponse: + if f.queryResp != nil { + proto.Merge(out, f.queryResp) + } + out.Error = f.dbError + case *abiv1.DbExecResponse: + out.RowsAffected = f.execRows + out.Error = f.dbError + case *abiv1.DbTxBeginResponse: + out.TxHandle = f.txHandle + out.Error = f.dbError + case *abiv1.DbTxCommitResponse: + out.Error = f.dbError + case *abiv1.DbTxRollbackResponse: + out.Error = f.dbError + } + return nil +} + +func (f *fakeHost) methods() []string { + ms := make([]string, len(f.calls)) + for i, c := range f.calls { + ms[i] = c.method + } + return ms +} + +func rowsWith(columns []string, cells ...*abiv1.DbValue) *abiv1.DbRowsResponse { + return &abiv1.DbRowsResponse{Columns: columns, Rows: []*abiv1.DbRow{{Values: cells}}} +} + +// deref returns the value a NewDest()-style pointer points at. +func deref(ptr any) any { return reflect.ValueOf(ptr).Elem().Interface() } + +func equalScan(want, got any) bool { + if w, ok := want.(time.Time); ok { + g, ok := got.(time.Time) + return ok && w.Equal(g) + } + return reflect.DeepEqual(want, got) +} + +// TestDbValueScanRoundTrip proves every DbValue variant round-trips through a +// SELECT and scans into its canonical Go type — the core contract WO-WZ-007 +// mirrors. Driven by the shared DbValueFixtures table. +func TestDbValueScanRoundTrip(t *testing.T) { + for _, fx := range DbValueFixtures { + t.Run(fx.Name, func(t *testing.T) { + host := &fakeHost{queryResp: rowsWith([]string{fx.Name}, fx.Value)} + pool := NewPool(host.call) + + dest := fx.NewDest() + if err := pool.QueryRow(context.Background(), "SELECT x").Scan(dest); err != nil { + t.Fatalf("scan %s: %v", fx.Name, err) + } + if got := deref(dest); !equalScan(fx.Want, got) { + t.Fatalf("scan %s: want %#v, got %#v", fx.Name, fx.Want, got) + } + }) + } +} + +// TestDbValueDriverValue proves the database/sql "bnwasm" driver maps every +// DbValue variant to the documented driver.Value. +func TestDbValueDriverValue(t *testing.T) { + for _, fx := range DbValueFixtures { + t.Run(fx.Name, func(t *testing.T) { + if got := driverValue(fx.Value); !equalScan(fx.DriverValue, got) { + t.Fatalf("driverValue %s: want %#v, got %#v", fx.Name, fx.DriverValue, got) + } + }) + } +} + +func TestExecRowsAffected(t *testing.T) { + host := &fakeHost{execRows: 5} + pool := NewPool(host.call) + + tag, err := pool.Exec(context.Background(), "UPDATE t SET x = 1") + if err != nil { + t.Fatal(err) + } + if tag.RowsAffected() != 5 { + t.Fatalf("rows affected: want 5, got %d", tag.RowsAffected()) + } + if host.calls[0].txHandle != 0 { + t.Fatalf("autocommit exec should carry tx_handle 0, got %d", host.calls[0].txHandle) + } +} + +// TestTxCommitSequence asserts the ordered host calls for a begin/exec/commit +// transaction, and that every statement inside carries the tx handle. +func TestTxCommitSequence(t *testing.T) { + host := &fakeHost{txHandle: 77, execRows: 1} + pool := NewPool(host.call) + ctx := context.Background() + + tx, err := pool.Begin(ctx) + if err != nil { + t.Fatal(err) + } + if _, err := tx.Exec(ctx, "INSERT INTO t VALUES ($1)", 1); err != nil { + t.Fatal(err) + } + if err := tx.Commit(ctx); err != nil { + t.Fatal(err) + } + + wantMethods := []string{methodTxBegin, methodExec, methodTxCommit} + if got := host.methods(); !reflect.DeepEqual(got, wantMethods) { + t.Fatalf("method sequence: want %v, got %v", wantMethods, got) + } + if host.calls[1].txHandle != 77 { + t.Fatalf("in-tx exec should carry handle 77, got %d", host.calls[1].txHandle) + } + if host.calls[2].txHandle != 77 { + t.Fatalf("commit should carry handle 77, got %d", host.calls[2].txHandle) + } +} + +// TestTxRollbackThenAutocommit asserts a rollback sequence, and that a query +// issued afterward (on the pool) carries NO tx handle. +func TestTxRollbackThenAutocommit(t *testing.T) { + host := &fakeHost{txHandle: 9, execRows: 1} + pool := NewPool(host.call) + ctx := context.Background() + + tx, err := pool.Begin(ctx) + if err != nil { + t.Fatal(err) + } + if err := tx.Rollback(ctx); err != nil { + t.Fatal(err) + } + if _, err := pool.Exec(ctx, "SELECT 1"); err != nil { + t.Fatal(err) + } + + wantMethods := []string{methodTxBegin, methodTxRollback, methodExec} + if got := host.methods(); !reflect.DeepEqual(got, wantMethods) { + t.Fatalf("method sequence: want %v, got %v", wantMethods, got) + } + if h := host.calls[1].txHandle; h != 9 { + t.Fatalf("rollback should carry handle 9, got %d", h) + } + if h := host.calls[2].txHandle; h != 0 { + t.Fatalf("post-rollback exec must carry tx_handle 0, got %d", h) + } +} + +func TestTxUseAfterCloseRejected(t *testing.T) { + host := &fakeHost{txHandle: 3} + tx, err := NewPool(host.call).Begin(context.Background()) + if err != nil { + t.Fatal(err) + } + if err := tx.Commit(context.Background()); err != nil { + t.Fatal(err) + } + if _, err := tx.Exec(context.Background(), "SELECT 1"); !errors.Is(err, pgx.ErrTxClosed) { + t.Fatalf("exec after commit: want ErrTxClosed, got %v", err) + } + if err := tx.Rollback(context.Background()); !errors.Is(err, pgx.ErrTxClosed) { + t.Fatalf("rollback after commit: want ErrTxClosed, got %v", err) + } +} + +func TestNestedTxRejected(t *testing.T) { + host := &fakeHost{txHandle: 1} + tx, err := NewPool(host.call).Begin(context.Background()) + if err != nil { + t.Fatal(err) + } + if _, err := tx.Begin(context.Background()); !errors.Is(err, errNestedTx) { + t.Fatalf("nested Begin: want errNestedTx, got %v", err) + } +} + +func TestNamedArgsRejected(t *testing.T) { + host := &fakeHost{} + pool := NewPool(host.call) + _, err := pool.Query(context.Background(), "SELECT $1", pgx.NamedArgs{"a": 1}) + if err == nil { + t.Fatal("expected named-args rejection, got nil") + } +} + +// TestDbErrorSurfacesAsPgError proves a DbError carrying a SQLSTATE reaches the +// plugin as a *pgconn.PgError, so errors.As-based constraint checks keep working. +func TestDbErrorSurfacesAsPgError(t *testing.T) { + host := &fakeHost{dbError: &abiv1.DbError{Code: "23505", Message: "duplicate key"}} + _, err := NewPool(host.call).Exec(context.Background(), "INSERT INTO t VALUES (1)") + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) { + t.Fatalf("want *pgconn.PgError, got %T (%v)", err, err) + } + if pgErr.Code != "23505" { + t.Fatalf("want SQLSTATE 23505, got %q", pgErr.Code) + } +} + +func TestNoHostFailsCleanly(t *testing.T) { + pool := NewPool(nil) + if _, err := pool.Exec(context.Background(), "SELECT 1"); !errors.Is(err, errNoHost) { + t.Fatalf("want errNoHost, got %v", err) + } +} + +func TestQueryRowNoRows(t *testing.T) { + host := &fakeHost{queryResp: &abiv1.DbRowsResponse{Columns: []string{"x"}}} + var x int + err := NewPool(host.call).QueryRow(context.Background(), "SELECT x").Scan(&x) + if !errors.Is(err, pgx.ErrNoRows) { + t.Fatalf("want ErrNoRows, got %v", err) + } +} + +// TestDatabaseSQLDriverRegistered proves the "bnwasm" driver is registered and +// drives a SELECT end-to-end through database/sql against the fake host. +func TestDatabaseSQLDriverRegistered(t *testing.T) { + host := &fakeHost{queryResp: rowsWith( + []string{"id", "name"}, + &abiv1.DbValue{Kind: &abiv1.DbValue_Int64Value{Int64Value: 1}}, + &abiv1.DbValue{Kind: &abiv1.DbValue_StringValue{StringValue: "alice"}}, + )} + db := sql.OpenDB(NewConnector(host.call)) + defer func() { _ = db.Close() }() + + var id int + var name string + if err := db.QueryRowContext(context.Background(), "SELECT id, name FROM t").Scan(&id, &name); err != nil { + t.Fatal(err) + } + if id != 1 || name != "alice" { + t.Fatalf("got id=%d name=%q", id, name) + } + if !slicesContains(sql.Drivers(), DriverName) { + t.Fatalf("driver %q not registered; have %v", DriverName, sql.Drivers()) + } +} + +func slicesContains(s []string, v string) bool { + return slices.Contains(s, v) +} diff --git a/plugin/wasmguest/bnwasm/pool.go b/plugin/wasmguest/bnwasm/pool.go new file mode 100644 index 0000000..2f45b7a --- /dev/null +++ b/plugin/wasmguest/bnwasm/pool.go @@ -0,0 +1,60 @@ +package bnwasm + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// Pool implements plugin.Pool (the pgx-flavored interface plugins bind their +// sqlc DBTX to) over the db.* host calls. It is also itself a valid DBTX: its +// Exec/Query/QueryRow run in autocommit (tx_handle 0), so a plugin can pass the +// Pool directly to sqlc's New(db) for non-transactional queries, exactly as it +// passes a *pgxpool.Pool today. +type Pool struct { + t Transport +} + +// NewPool returns a Pool bound to transport t. A nil t (native build / DESCRIBE +// probe) yields a Pool whose every operation fails cleanly with errNoHost. +func NewPool(t Transport) *Pool { return &Pool{t: t} } + +// Begin opens a host-side transaction and returns a pgx.Tx bound to its handle. +func (p *Pool) Begin(ctx context.Context) (pgx.Tx, error) { + handle, err := p.t.txBegin(ctx) + if err != nil { + return nil, err + } + return &Tx{t: p.t, handle: handle}, nil +} + +// Exec runs a statement in autocommit and returns its command tag. +func (p *Pool) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + return p.t.exec(ctx, sql, 0, args) +} + +// Query runs a rows-returning statement in autocommit. +func (p *Pool) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { + resp, err := p.t.query(ctx, sql, 0, args) + if err != nil { + return errRows(err), err + } + return newPgxRows(resp), nil +} + +// QueryRow runs a rows-returning statement in autocommit and returns the first +// row; any error is deferred to Row.Scan, matching pgx. +func (p *Pool) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row { + resp, err := p.t.query(ctx, sql, 0, args) + if err != nil { + return &pgxRow{err: err} + } + return &pgxRow{rows: newPgxRows(resp)} +} + +// errRows is a closed, empty pgx.Rows carrying err, so a Query caller that +// ignores the returned error and iterates still terminates and reports it. +func errRows(err error) *pgxRows { + return &pgxRows{idx: -1, err: err, closed: true} +} diff --git a/plugin/wasmguest/bnwasm/rows.go b/plugin/wasmguest/bnwasm/rows.go new file mode 100644 index 0000000..1816423 --- /dev/null +++ b/plugin/wasmguest/bnwasm/rows.go @@ -0,0 +1,120 @@ +package bnwasm + +import ( + "errors" + "fmt" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// pgxRows adapts a buffered DbRowsResponse to the pgx.Rows interface. The full +// result set is already materialized (the host returns it in one message), so +// iteration is a slice cursor. Only the methods sqlc-generated code exercises +// carry real behavior; the rest satisfy the interface. +type pgxRows struct { + columns []string + rows []*abiv1.DbRow + idx int // index of the row Next() last advanced to; -1 before first Next + err error + closed bool +} + +func newPgxRows(resp *abiv1.DbRowsResponse) *pgxRows { + return &pgxRows{columns: resp.GetColumns(), rows: resp.GetRows(), idx: -1} +} + +func (r *pgxRows) Close() { r.closed = true } + +func (r *pgxRows) Err() error { return r.err } + +func (r *pgxRows) CommandTag() pgconn.CommandTag { + return pgconn.NewCommandTag(fmt.Sprintf("SELECT %d", len(r.rows))) +} + +func (r *pgxRows) FieldDescriptions() []pgconn.FieldDescription { + fds := make([]pgconn.FieldDescription, len(r.columns)) + for i, c := range r.columns { + fds[i] = pgconn.FieldDescription{Name: c} + } + return fds +} + +func (r *pgxRows) Next() bool { + if r.err != nil || r.closed { + return false + } + if r.idx+1 >= len(r.rows) { + r.Close() // pgx auto-closes when iteration is exhausted + return false + } + r.idx++ + return true +} + +func (r *pgxRows) Scan(dest ...any) error { + if r.err != nil { + return r.err + } + if r.idx < 0 || r.idx >= len(r.rows) { + return errors.New("bnwasm: Scan called without a current row (call Next first)") + } + if err := scanRow(r.rows[r.idx], dest); err != nil { + r.err = err + r.Close() + return err + } + return nil +} + +func (r *pgxRows) Values() ([]any, error) { + if r.idx < 0 || r.idx >= len(r.rows) { + return nil, errors.New("bnwasm: Values called without a current row") + } + cells := r.rows[r.idx].GetValues() + out := make([]any, len(cells)) + for i, c := range cells { + v, _ := naturalValue(c) + out[i] = v + } + return out, nil +} + +func (r *pgxRows) RawValues() [][]byte { return nil } + +func (r *pgxRows) Conn() *pgx.Conn { return nil } + +// pgxRow is the single-row QueryRow result. Like pgx, it defers all errors +// (including the query error) to Scan, and reports pgx.ErrNoRows when empty. +type pgxRow struct { + rows *pgxRows + err error +} + +func (r *pgxRow) Scan(dest ...any) error { + if r.err != nil { + return r.err + } + defer r.rows.Close() + if !r.rows.Next() { + if r.rows.Err() != nil { + return r.rows.Err() + } + return pgx.ErrNoRows + } + return r.rows.Scan(dest...) +} + +func scanRow(row *abiv1.DbRow, dest []any) error { + cells := row.GetValues() + if len(dest) != len(cells) { + return fmt.Errorf("bnwasm: scan expected %d destinations, got %d columns", len(dest), len(cells)) + } + for i, d := range dest { + if err := scanValue(cells[i], d); err != nil { + return fmt.Errorf("bnwasm: scan column %d: %w", i, err) + } + } + return nil +} diff --git a/plugin/wasmguest/bnwasm/sqlcgen_test.go b/plugin/wasmguest/bnwasm/sqlcgen_test.go new file mode 100644 index 0000000..5c5c8d1 --- /dev/null +++ b/plugin/wasmguest/bnwasm/sqlcgen_test.go @@ -0,0 +1,207 @@ +package bnwasm + +import ( + "context" + "testing" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" +) + +// --- Vendored sqlc output --------------------------------------------------- +// +// The block below is byte-for-byte in the shape `sqlc generate` emits for the +// pgx/v5 sql_package with the same overrides symposium uses (uuid→uuid.UUID, +// jsonb→json.RawMessage, emit_pointers_for_null_types). It is checked in so a +// compile of THIS test is proof that generated plugin code binds to the bnwasm +// Pool/Tx (as its DBTX) and pgxRows/pgxRow (via Scan) with no edits — the same +// DBTX interface, the same pgconn.CommandTag / pgx.Rows / pgx.Row signatures. + +type DBTX interface { + Exec(context.Context, string, ...any) (pgconn.CommandTag, error) + Query(context.Context, string, ...any) (pgx.Rows, error) + QueryRow(context.Context, string, ...any) pgx.Row +} + +func New(db DBTX) *Queries { return &Queries{db: db} } + +type Queries struct{ db DBTX } + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { return &Queries{db: tx} } + +type Widget struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Notes *string `json:"notes"` + Count int32 `json:"count"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +const createWidget = `-- name: CreateWidget :one +INSERT INTO widgets (name, notes) VALUES ($1, $2) RETURNING id +` + +type CreateWidgetParams struct { + Name string `json:"name"` + Notes *string `json:"notes"` +} + +func (q *Queries) CreateWidget(ctx context.Context, arg CreateWidgetParams) (uuid.UUID, error) { + row := q.db.QueryRow(ctx, createWidget, arg.Name, arg.Notes) + var id uuid.UUID + err := row.Scan(&id) + return id, err +} + +const getWidget = `-- name: GetWidget :one +SELECT id, name, notes, count, created_at FROM widgets WHERE id = $1 +` + +func (q *Queries) GetWidget(ctx context.Context, id uuid.UUID) (Widget, error) { + row := q.db.QueryRow(ctx, getWidget, id) + var i Widget + err := row.Scan(&i.ID, &i.Name, &i.Notes, &i.Count, &i.CreatedAt) + return i, err +} + +const listWidgets = `-- name: ListWidgets :many +SELECT id, name, notes, count, created_at FROM widgets ORDER BY name +` + +func (q *Queries) ListWidgets(ctx context.Context) ([]Widget, error) { + rows, err := q.db.Query(ctx, listWidgets) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Widget + for rows.Next() { + var i Widget + if err := rows.Scan(&i.ID, &i.Name, &i.Notes, &i.Count, &i.CreatedAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const deleteWidget = `-- name: DeleteWidget :exec +DELETE FROM widgets WHERE id = $1 +` + +func (q *Queries) DeleteWidget(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteWidget, id) + return err +} + +// --- Compile-time proof the bnwasm surfaces satisfy the generated interfaces -- + +var ( + _ DBTX = (*Pool)(nil) + _ DBTX = (*Tx)(nil) + _ pgx.Tx = (*Tx)(nil) +) + +// --- End-to-end runs against the fake host ---------------------------------- + +func widgetRow(id uuid.UUID, name string, count int32) *abiv1.DbRow { + return &abiv1.DbRow{Values: []*abiv1.DbValue{ + {Kind: &abiv1.DbValue_UuidValue{UuidValue: id.String()}}, + {Kind: &abiv1.DbValue_StringValue{StringValue: name}}, + {Kind: &abiv1.DbValue_Null{Null: true}}, // notes → NULL → (*string)(nil) + {Kind: &abiv1.DbValue_Int64Value{Int64Value: int64(count)}}, + {Kind: &abiv1.DbValue_Null{Null: true}}, // created_at → NULL Timestamptz + }} +} + +func TestSqlcGetWidget(t *testing.T) { + id := uuid.New() + host := &fakeHost{queryResp: &abiv1.DbRowsResponse{ + Columns: []string{"id", "name", "notes", "count", "created_at"}, + Rows: []*abiv1.DbRow{widgetRow(id, "gadget", 3)}, + }} + q := New(NewPool(host.call)) + + w, err := q.GetWidget(context.Background(), id) + if err != nil { + t.Fatal(err) + } + if w.ID != id || w.Name != "gadget" || w.Count != 3 { + t.Fatalf("unexpected widget: %+v", w) + } + if w.Notes != nil { + t.Fatalf("notes should be nil for NULL column, got %v", *w.Notes) + } + if w.CreatedAt.Valid { + t.Fatalf("created_at should be invalid for NULL column") + } +} + +func TestSqlcListWidgets(t *testing.T) { + host := &fakeHost{queryResp: &abiv1.DbRowsResponse{ + Columns: []string{"id", "name", "notes", "count", "created_at"}, + Rows: []*abiv1.DbRow{ + widgetRow(uuid.New(), "alpha", 1), + widgetRow(uuid.New(), "beta", 2), + }, + }} + q := New(NewPool(host.call)) + + items, err := q.ListWidgets(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(items) != 2 || items[0].Name != "alpha" || items[1].Name != "beta" { + t.Fatalf("unexpected list: %+v", items) + } +} + +// TestSqlcWithTxCreate proves the generated WithTx path binds to the bnwasm Tx +// and that CreateWidget's RETURNING id scans, all inside one transaction with +// the right ordered host calls. +func TestSqlcWithTxCreate(t *testing.T) { + newID := uuid.New() + host := &fakeHost{ + txHandle: 42, + queryResp: &abiv1.DbRowsResponse{ + Columns: []string{"id"}, + Rows: []*abiv1.DbRow{{Values: []*abiv1.DbValue{{Kind: &abiv1.DbValue_UuidValue{UuidValue: newID.String()}}}}}, + }, + } + pool := NewPool(host.call) + ctx := context.Background() + + tx, err := pool.Begin(ctx) + if err != nil { + t.Fatal(err) + } + notes := "hi" + gotID, err := New(pool).WithTx(tx).CreateWidget(ctx, CreateWidgetParams{Name: "w", Notes: ¬es}) + if err != nil { + t.Fatal(err) + } + if gotID != newID { + t.Fatalf("returning id: want %s got %s", newID, gotID) + } + if err := tx.Commit(ctx); err != nil { + t.Fatal(err) + } + + // begin → query(RETURNING) → commit; the query carried the tx handle. + if got := host.methods(); len(got) != 3 || got[0] != methodTxBegin || got[1] != methodQuery || got[2] != methodTxCommit { + t.Fatalf("method sequence: %v", got) + } + if host.calls[1].txHandle != 42 { + t.Fatalf("in-tx query should carry handle 42, got %d", host.calls[1].txHandle) + } + // The NULL-able *string arg marshaled as a real string arg. + if len(host.calls[1].args) != 2 { + t.Fatalf("want 2 args, got %d", len(host.calls[1].args)) + } +} diff --git a/plugin/wasmguest/bnwasm/transport.go b/plugin/wasmguest/bnwasm/transport.go new file mode 100644 index 0000000..0a1019b --- /dev/null +++ b/plugin/wasmguest/bnwasm/transport.go @@ -0,0 +1,194 @@ +// Package bnwasm is the guest-side database access layer for wazero-loaded +// BlockNinja plugins. It presents two surfaces over the SAME db.* host calls +// (abiv1 db.proto), so plugin code that talks to Postgres keeps compiling and +// running unchanged inside the wasm sandbox: +// +// - A database/sql/driver registered as "bnwasm" (driver.go), for plugins or +// sqlc configs that use the standard library. +// - A plugin.Pool implementation (pool.go) that hands out a pgx.Tx-shaped +// value (tx.go), so the pgx-flavored sqlc DBTX interface every current +// plugin generates against (sql_package: "pgx/v5") is satisfied with no +// source edits. This is the PRIMARY path: symposium/messenger sqlc output +// uses pgconn.CommandTag / pgx.Rows / pgx.Row, which database/sql cannot +// produce, so the pgx surface is what their generated db packages bind to. +// +// # The transport seam +// +// Every DB operation is "marshal a db. request, invoke it, unmarshal the +// reply". That transport is a Transport func injected at construction, mirroring +// caps.CallFunc exactly, so the marshaling/scanning logic here stays natively +// testable with a fake host (driver_test.go, sqlcgen_test.go) while the wasm +// guest shim binds the real host_call-backed transport (wired from package +// wasmguest, which imports this package — never the reverse). A nil Transport +// (native builds, DESCRIBE probes) fails every call cleanly with errNoHost +// instead of nil-panicking, matching package caps. +package bnwasm + +import ( + "context" + "errors" + "fmt" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/jackc/pgx/v5/pgconn" + "google.golang.org/protobuf/proto" +) + +// Transport is the guest→host DB transport: marshal req, invoke the host with +// the db.* method, and unmarshal the reply into resp. It is structurally +// identical to caps.CallFunc so the wasip1 shim can bind one func to both. +type Transport func(method string, req, resp proto.Message) error + +// db.* method names (see core/docs/wasm-abi.md §"Capability calls"). +const ( + methodQuery = "db.query" + methodExec = "db.exec" + methodTxBegin = "db.tx_begin" + methodTxCommit = "db.tx_commit" + methodTxRollback = "db.tx_rollback" +) + +// errNoHost is returned by every operation when no transport is bound (native +// build / DESCRIBE probe). It is distinct so tests can assert on it. +var errNoHost = errors.New("bnwasm: no host transport bound (native build)") + +// dbErr maps a DbError from a response into a *pgconn.PgError, the same error +// type pgx surfaces, so plugin code that does errors.As(err, &pgErr) to inspect +// a SQLSTATE (e.g. "23505" unique_violation) keeps working across the sandbox. +func dbErr(e *abiv1.DbError) error { + if e == nil { + return nil + } + return &pgconn.PgError{Code: e.GetCode(), Message: e.GetMessage()} +} + +// ErrTxExpired is returned by a db.* call whose transaction handle the host +// has already expired — it drops the handle at the call-chain deadline +// (WO-WZ-007) so a guest can never pin a connection. Unlike a real fault this +// is retryable: re-run the unit of work in a fresh transaction. The host +// signals it with ABI_ERROR_CODE_TX_EXPIRED (cms dbexec side, adopted +// separately); the transport wraps ErrTxExpired around the raw host error so +// callers can `errors.Is(err, bnwasm.ErrTxExpired)`. +var ErrTxExpired = errors.New("bnwasm: transaction expired; retry in a new transaction") + +// abiCoded is satisfied by *wasmguest.HostError (its AbiErrorCode method) +// without importing that wasip1-only package, so bnwasm can classify a +// transport error by its ABI code across the sandbox boundary. +type abiCoded interface { + AbiErrorCode() abiv1.AbiErrorCode +} + +// mapTransportErr translates a transport-level ABI error into a bnwasm +// sentinel where one exists (TX_EXPIRED → ErrTxExpired), leaving every other +// error untouched. Applied to the raw t(...) error at each db.* call site. +func mapTransportErr(err error) error { + if err == nil { + return nil + } + var coded abiCoded + if errors.As(err, &coded) && coded.AbiErrorCode() == abiv1.AbiErrorCode_ABI_ERROR_CODE_TX_EXPIRED { + return fmt.Errorf("%w: %v", ErrTxExpired, err) + } + return err +} + +func (t Transport) query(ctx context.Context, sql string, txHandle uint64, args []any) (*abiv1.DbRowsResponse, error) { + if t == nil { + return nil, errNoHost + } + if err := ctxErr(ctx); err != nil { + return nil, err + } + vals, err := toDbValues(args) + if err != nil { + return nil, err + } + resp := &abiv1.DbRowsResponse{} + if err := t(methodQuery, &abiv1.DbQueryRequest{Sql: sql, Args: vals, TxHandle: txHandle}, resp); err != nil { + return nil, mapTransportErr(err) + } + if err := dbErr(resp.GetError()); err != nil { + return nil, err + } + return resp, nil +} + +func (t Transport) exec(ctx context.Context, sql string, txHandle uint64, args []any) (pgconn.CommandTag, error) { + if t == nil { + return pgconn.CommandTag{}, errNoHost + } + if err := ctxErr(ctx); err != nil { + return pgconn.CommandTag{}, err + } + vals, err := toDbValues(args) + if err != nil { + return pgconn.CommandTag{}, err + } + resp := &abiv1.DbExecResponse{} + if err := t(methodExec, &abiv1.DbExecRequest{Sql: sql, Args: vals, TxHandle: txHandle}, resp); err != nil { + return pgconn.CommandTag{}, mapTransportErr(err) + } + if err := dbErr(resp.GetError()); err != nil { + return pgconn.CommandTag{}, err + } + // Encode rows-affected in a CommandTag whose trailing integer pgconn parses + // back out via RowsAffected() — the only field sqlc-generated code reads. + return pgconn.NewCommandTag(fmt.Sprintf("EXEC %d", resp.GetRowsAffected())), nil +} + +func (t Transport) txBegin(ctx context.Context) (uint64, error) { + if t == nil { + return 0, errNoHost + } + if err := ctxErr(ctx); err != nil { + return 0, err + } + resp := &abiv1.DbTxBeginResponse{} + if err := t(methodTxBegin, &abiv1.DbTxBeginRequest{}, resp); err != nil { + return 0, mapTransportErr(err) + } + if err := dbErr(resp.GetError()); err != nil { + return 0, err + } + if resp.GetTxHandle() == 0 { + return 0, errors.New("bnwasm: host returned tx_handle 0 (never valid)") + } + return resp.GetTxHandle(), nil +} + +func (t Transport) txCommit(ctx context.Context, handle uint64) error { + if t == nil { + return errNoHost + } + if err := ctxErr(ctx); err != nil { + return err + } + resp := &abiv1.DbTxCommitResponse{} + if err := t(methodTxCommit, &abiv1.DbTxCommitRequest{TxHandle: handle}, resp); err != nil { + return mapTransportErr(err) + } + return dbErr(resp.GetError()) +} + +func (t Transport) txRollback(ctx context.Context, handle uint64) error { + if t == nil { + return errNoHost + } + if err := ctxErr(ctx); err != nil { + return err + } + resp := &abiv1.DbTxRollbackResponse{} + if err := t(methodTxRollback, &abiv1.DbTxRollbackRequest{TxHandle: handle}, resp); err != nil { + return mapTransportErr(err) + } + return dbErr(resp.GetError()) +} + +// ctxErr short-circuits an already-cancelled/expired context before crossing +// the boundary, matching the caps stubs; the host enforces the invoke deadline. +func ctxErr(ctx context.Context) error { + if ctx == nil { + return nil + } + return ctx.Err() +} diff --git a/plugin/wasmguest/bnwasm/tx.go b/plugin/wasmguest/bnwasm/tx.go new file mode 100644 index 0000000..08d1aae --- /dev/null +++ b/plugin/wasmguest/bnwasm/tx.go @@ -0,0 +1,102 @@ +package bnwasm + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// errNestedTx is returned by Tx.Begin. pgx models a nested Begin as a SAVEPOINT; +// the db.* ABI has no savepoint verb (v1 scope), and a grep of the plugin fleet +// (symposium, messenger) found no nested-transaction/savepoint use, so this is +// rejected explicitly rather than silently degrading correctness. +var errNestedTx = errors.New("bnwasm: nested transactions (savepoints) are not supported") + +// Tx is a pgx.Tx bound to a host-side transaction handle. Every Exec/Query/ +// QueryRow carries the handle so the host runs it on the transaction's +// connection; Commit/Rollback release it. After either, the Tx is closed and +// further statements return pgx.ErrTxClosed. The host also drops the handle at +// the call chain's deadline (WO-WZ-007), so a guest that leaks a Tx without +// committing has it rolled back host-side — a guest can never pin a connection. +type Tx struct { + t Transport + handle uint64 + closed bool +} + +// Begin would start a savepoint-backed nested transaction: unsupported. +func (tx *Tx) Begin(ctx context.Context) (pgx.Tx, error) { return nil, errNestedTx } + +func (tx *Tx) Commit(ctx context.Context) error { + if tx.closed { + return pgx.ErrTxClosed + } + tx.closed = true + return tx.t.txCommit(ctx, tx.handle) +} + +func (tx *Tx) Rollback(ctx context.Context) error { + if tx.closed { + return pgx.ErrTxClosed + } + tx.closed = true + return tx.t.txRollback(ctx, tx.handle) +} + +func (tx *Tx) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + if tx.closed { + return pgconn.CommandTag{}, pgx.ErrTxClosed + } + return tx.t.exec(ctx, sql, tx.handle, args) +} + +func (tx *Tx) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { + if tx.closed { + return errRows(pgx.ErrTxClosed), pgx.ErrTxClosed + } + resp, err := tx.t.query(ctx, sql, tx.handle, args) + if err != nil { + return errRows(err), err + } + return newPgxRows(resp), nil +} + +func (tx *Tx) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row { + if tx.closed { + return &pgxRow{err: pgx.ErrTxClosed} + } + resp, err := tx.t.query(ctx, sql, tx.handle, args) + if err != nil { + return &pgxRow{err: err} + } + return &pgxRow{rows: newPgxRows(resp)} +} + +// --- Unsupported pgx.Tx surface (not emitted by sqlc; explicit errors) --- + +func (tx *Tx) CopyFrom(context.Context, pgx.Identifier, []string, pgx.CopyFromSource) (int64, error) { + return 0, errors.New("bnwasm: CopyFrom is not supported over the wasm DB ABI") +} + +func (tx *Tx) SendBatch(context.Context, *pgx.Batch) pgx.BatchResults { + return errBatchResults{errors.New("bnwasm: SendBatch is not supported over the wasm DB ABI")} +} + +func (tx *Tx) LargeObjects() pgx.LargeObjects { return pgx.LargeObjects{} } + +func (tx *Tx) Prepare(context.Context, string, string) (*pgconn.StatementDescription, error) { + return nil, errors.New("bnwasm: Prepare is not supported over the wasm DB ABI") +} + +func (tx *Tx) Conn() *pgx.Conn { return nil } + +// errBatchResults is a pgx.BatchResults that reports the same error from every +// method, so an unsupported SendBatch surfaces cleanly instead of nil-panicking. +type errBatchResults struct{ err error } + +func (e errBatchResults) Exec() (pgconn.CommandTag, error) { return pgconn.CommandTag{}, e.err } +func (e errBatchResults) Query() (pgx.Rows, error) { return errRows(e.err), e.err } +func (e errBatchResults) QueryRow() pgx.Row { return &pgxRow{err: e.err} } +func (e errBatchResults) Close() error { return e.err } diff --git a/plugin/wasmguest/bnwasm/tx_expired_test.go b/plugin/wasmguest/bnwasm/tx_expired_test.go new file mode 100644 index 0000000..4c2e75e --- /dev/null +++ b/plugin/wasmguest/bnwasm/tx_expired_test.go @@ -0,0 +1,74 @@ +package bnwasm + +import ( + "context" + "errors" + "testing" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "google.golang.org/protobuf/proto" +) + +// fakeHostErr mirrors *wasmguest.HostError's AbiErrorCode() method so the +// transport's cross-boundary classification (mapTransportErr) can be exercised +// natively, without importing the wasip1-only wasmguest package. +type fakeHostErr struct { + code abiv1.AbiErrorCode + msg string +} + +func (e *fakeHostErr) Error() string { return e.msg } +func (e *fakeHostErr) AbiErrorCode() abiv1.AbiErrorCode { return e.code } + +// transportReturning builds a Transport whose every call fails with err. +func transportReturning(err error) Transport { + return func(_ string, _, _ proto.Message) error { return err } +} + +func TestTransportMapsTxExpiredToSentinel(t *testing.T) { + hostErr := &fakeHostErr{code: abiv1.AbiErrorCode_ABI_ERROR_CODE_TX_EXPIRED, msg: "host: tx handle expired"} + tr := transportReturning(hostErr) + ctx := context.Background() + + // Every db.* verb that crosses the transport must classify TX_EXPIRED. + t.Run("query", func(t *testing.T) { + _, err := tr.query(ctx, "SELECT 1", 7, nil) + if !errors.Is(err, ErrTxExpired) { + t.Fatalf("query err = %v, want ErrTxExpired", err) + } + }) + t.Run("exec", func(t *testing.T) { + _, err := tr.exec(ctx, "DELETE FROM t", 7, nil) + if !errors.Is(err, ErrTxExpired) { + t.Fatalf("exec err = %v, want ErrTxExpired", err) + } + }) + t.Run("txBegin", func(t *testing.T) { + _, err := tr.txBegin(ctx) + if !errors.Is(err, ErrTxExpired) { + t.Fatalf("txBegin err = %v, want ErrTxExpired", err) + } + }) + t.Run("txCommit", func(t *testing.T) { + if err := tr.txCommit(ctx, 7); !errors.Is(err, ErrTxExpired) { + t.Fatalf("txCommit err = %v, want ErrTxExpired", err) + } + }) + t.Run("txRollback", func(t *testing.T) { + if err := tr.txRollback(ctx, 7); !errors.Is(err, ErrTxExpired) { + t.Fatalf("txRollback err = %v, want ErrTxExpired", err) + } + }) +} + +func TestTransportLeavesOtherAbiErrorsUntouched(t *testing.T) { + hostErr := &fakeHostErr{code: abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, msg: "boom"} + tr := transportReturning(hostErr) + _, err := tr.query(context.Background(), "SELECT 1", 0, nil) + if errors.Is(err, ErrTxExpired) { + t.Fatalf("non-tx-expired error was misclassified as ErrTxExpired: %v", err) + } + if !errors.Is(err, error(hostErr)) { + t.Fatalf("original host error not preserved: %v", err) + } +} diff --git a/plugin/wasmguest/caps/ai.go b/plugin/wasmguest/caps/ai.go new file mode 100644 index 0000000..6ed7158 --- /dev/null +++ b/plugin/wasmguest/caps/ai.go @@ -0,0 +1,69 @@ +package caps + +import ( + "context" + "encoding/json" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/ai" +) + +// aiStub implements ai.ToolRegistry (ai.tools.register) and backs the +// CoreServices.AITextCall func field (ai.text_call). +// +// ToolDefinition.Handler stays guest-side: registration marshals only the +// static descriptor AND records the full definition locally so the host can +// execute the Handler via HOOK_AI_TOOL_CALL. Register tools in Register (every +// pooled instance runs it), not Load (one instance only) — see +// core/docs/wasm-abi.md §"Per-instance state". Instances are single-threaded, +// so the plain map needs no locking. +type aiStub struct { + base + tools map[string]*ai.ToolDefinition // slug → definition (with Handler) +} + +var _ ai.ToolRegistry = (*aiStub)(nil) + +// Register has no error channel; a transport failure is dropped (the host +// records nothing, matching the "best effort at load" contract). +func (s *aiStub) Register(tool *ai.ToolDefinition) { + if tool == nil { + return + } + if s.tools == nil { + s.tools = make(map[string]*ai.ToolDefinition) + } + s.tools[tool.Slug] = tool + req := &abiv1.AiToolRegisterRequest{ + Slug: tool.Slug, + Name: tool.Name, + Description: tool.Description, + } + if tool.ParameterSchema != nil { + if raw, err := json.Marshal(tool.ParameterSchema); err == nil { + req.ParameterSchemaJson = raw + } + } + _ = s.invoke(context.Background(), "tools.register", req, &abiv1.AiToolRegisterResponse{}) +} + +// Tool returns the registered tool definition for a slug, for +// HOOK_AI_TOOL_CALL dispatch by the wasm shim. +func (s *aiStub) Tool(slug string) (*ai.ToolDefinition, bool) { + t, ok := s.tools[slug] + return t, ok +} + +// textCall backs CoreServices.AITextCall. +func (s *aiStub) textCall(ctx context.Context, taskKey, systemPrompt, userMessage string) (string, error) { + req := &abiv1.AiTextCallRequest{ + TaskKey: taskKey, + SystemPrompt: systemPrompt, + UserMessage: userMessage, + } + resp := &abiv1.AiTextCallResponse{} + if err := s.invoke(ctx, "text_call", req, resp); err != nil { + return "", err + } + return resp.GetText(), nil +} diff --git a/plugin/wasmguest/caps/author.go b/plugin/wasmguest/caps/author.go new file mode 100644 index 0000000..d768950 --- /dev/null +++ b/plugin/wasmguest/caps/author.go @@ -0,0 +1,120 @@ +package caps + +import ( + "context" + "encoding/json" + "fmt" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/content" + "github.com/google/uuid" +) + +// authorStub implements content.Author over the content.* write capability +// calls (WO-WZ-019). Same family as contentStub; split so the read surface +// stays untouched. +type authorStub struct{ base } + +var _ content.Author = (*authorStub)(nil) + +func (s *authorStub) CreatePage(ctx context.Context, params content.CreatePageParams) (content.CreatePageResult, error) { + req := &abiv1.ContentCreatePageRequest{ + Slug: params.Slug, + ParentSlug: params.ParentSlug, + Title: params.Title, + TemplateKey: params.TemplateKey, + MasterPageKey: params.MasterPageKey, + } + resp := &abiv1.ContentCreatePageResponse{} + if err := s.invoke(ctx, "create_page", req, resp); err != nil { + return content.CreatePageResult{}, err + } + id, err := uuid.Parse(resp.GetPageId()) + if err != nil { + return content.CreatePageResult{}, fmt.Errorf("content.create_page: bad page_id %q: %w", resp.GetPageId(), err) + } + return content.CreatePageResult{PageID: id, Created: resp.GetCreated()}, nil +} + +func (s *authorStub) SetPageBlocks(ctx context.Context, pageID uuid.UUID, blocks []content.PageBlock) error { + req := &abiv1.ContentSetPageBlocksRequest{PageId: pageID.String()} + for _, b := range blocks { + pb, err := pageBlockToProto(b) + if err != nil { + return fmt.Errorf("content.set_page_blocks: block %q: %w", b.BlockKey, err) + } + req.Blocks = append(req.Blocks, pb) + } + return s.invoke(ctx, "set_page_blocks", req, &abiv1.ContentSetPageBlocksResponse{}) +} + +func (s *authorStub) PublishPage(ctx context.Context, pageID uuid.UUID) error { + req := &abiv1.ContentPublishPageRequest{PageId: pageID.String()} + return s.invoke(ctx, "publish_page", req, &abiv1.ContentPublishPageResponse{}) +} + +func (s *authorStub) SetPageSEO(ctx context.Context, pageID uuid.UUID, seo content.PageSEO) error { + req := &abiv1.ContentSetPageSeoRequest{ + PageId: pageID.String(), + MetaTitle: seo.MetaTitle, + MetaDescription: seo.MetaDescription, + OgTitle: seo.OGTitle, + OgDescription: seo.OGDescription, + OgImage: seo.OGImage, + FocusKeyphrase: seo.FocusKeyphrase, + CanonicalUrl: seo.CanonicalURL, + RobotsDirective: seo.RobotsDirective, + TwitterTitle: seo.TwitterTitle, + TwitterDescription: seo.TwitterDescription, + TwitterImage: seo.TwitterImage, + } + return s.invoke(ctx, "set_page_seo", req, &abiv1.ContentSetPageSeoResponse{}) +} + +func (s *authorStub) UpsertPost(ctx context.Context, params content.UpsertPostParams) (content.UpsertPostResult, error) { + docJSON, err := json.Marshal(params.Document) + if err != nil { + return content.UpsertPostResult{}, fmt.Errorf("content.upsert_post: marshal document: %w", err) + } + req := &abiv1.ContentUpsertPostRequest{ + Slug: params.Slug, + Title: params.Title, + DocumentJson: docJSON, + Excerpt: params.Excerpt, + Publish: params.Publish, + IsFeatured: params.IsFeatured, + } + if params.AuthorProfileID != nil { + v := params.AuthorProfileID.String() + req.AuthorProfileId = &v + } + if params.FeaturedImageID != nil { + v := params.FeaturedImageID.String() + req.FeaturedImageId = &v + } + resp := &abiv1.ContentUpsertPostResponse{} + if err := s.invoke(ctx, "upsert_post", req, resp); err != nil { + return content.UpsertPostResult{}, err + } + id, err := uuid.Parse(resp.GetPostId()) + if err != nil { + return content.UpsertPostResult{}, fmt.Errorf("content.upsert_post: bad post_id %q: %w", resp.GetPostId(), err) + } + return content.UpsertPostResult{PostID: id, Created: resp.GetCreated()}, nil +} + +// pageBlockToProto converts one content.PageBlock to its wire form. +func pageBlockToProto(b content.PageBlock) (*abiv1.PageBlock, error) { + contentJSON, err := json.Marshal(b.Content) + if err != nil { + return nil, err + } + return &abiv1.PageBlock{ + BlockKey: b.BlockKey, + Title: b.Title, + ContentJson: contentJSON, + HtmlContent: b.HTMLContent, + Slot: b.Slot, + SortOrder: b.SortOrder, + }, nil +} diff --git a/plugin/wasmguest/caps/badges.go b/plugin/wasmguest/caps/badges.go new file mode 100644 index 0000000..7ef1874 --- /dev/null +++ b/plugin/wasmguest/caps/badges.go @@ -0,0 +1,20 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "github.com/google/uuid" +) + +// badgesStub implements plugin.BadgeRefresher over the badges.refresh_badges +// capability call. +type badgesStub struct{ base } + +var _ plugin.BadgeRefresher = (*badgesStub)(nil) + +func (s *badgesStub) RefreshBadges(ctx context.Context, tableID, rowID uuid.UUID) error { + req := &abiv1.BadgesRefreshBadgesRequest{TableId: tableID.String(), RowId: rowID.String()} + return s.invoke(ctx, "refresh_badges", req, &abiv1.BadgesRefreshBadgesResponse{}) +} diff --git a/plugin/wasmguest/caps/bridge.go b/plugin/wasmguest/caps/bridge.go new file mode 100644 index 0000000..67aab61 --- /dev/null +++ b/plugin/wasmguest/caps/bridge.go @@ -0,0 +1,74 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" +) + +// bridgeStub implements plugin.PluginBridge over bridge.* capability calls. +// +// The bridge shares in-process Go values today; across sandboxes only the +// registration/lookup *surface* serializes — the service value itself cannot +// cross. RegisterService therefore forwards only plugin/service names to the +// host AND records the value locally so this plugin's own services are +// dispatchable via HOOK_BRIDGE_CALL; GetService returns nil for remote +// services (availability is checked but a typed value cannot be reconstructed +// guest-side). Cross-plugin calls use Invoke (bridge.invoke) with opaque +// payloads instead. +// +// Instances are single-threaded, so the plain map needs no locking; register +// bridge services in Register (every pooled instance runs it), not Load +// (which fires on one instance only) — see core/docs/wasm-abi.md +// §"Per-instance state". +type bridgeStub struct { + base + services map[string]any // serviceName → registered value (this plugin's own) +} + +var _ plugin.PluginBridge = (*bridgeStub)(nil) + +// RegisterService has no error channel; a transport failure is dropped. +func (s *bridgeStub) RegisterService(pluginName, serviceName string, service any) { + if s.services == nil { + s.services = make(map[string]any) + } + s.services[serviceName] = service + req := &abiv1.BridgeRegisterServiceRequest{PluginName: pluginName, ServiceName: serviceName} + _ = s.invoke(context.Background(), "register_service", req, &abiv1.BridgeRegisterServiceResponse{}) +} + +// GetService reports availability host-side but cannot return the concrete Go +// value across the sandbox boundary, so it always returns nil for remote +// services. Callers using plugin.GetServiceAs correctly observe (zero, false); +// cross-plugin calls go through Invoke. +func (s *bridgeStub) GetService(pluginName, serviceName string) any { + req := &abiv1.BridgeGetServiceRequest{PluginName: pluginName, ServiceName: serviceName} + _ = s.invoke(context.Background(), "get_service", req, &abiv1.BridgeGetServiceResponse{}) + return nil +} + +// Invoke calls a method on another plugin's registered bridge service with an +// opaque payload (bridge.invoke; the provider answers via HOOK_BRIDGE_CALL or +// its in-process plugin.BridgeInvokable). +func (s *bridgeStub) Invoke(ctx context.Context, pluginName, serviceName, method string, payload []byte) ([]byte, error) { + req := &abiv1.BridgeInvokeRequest{ + PluginName: pluginName, + ServiceName: serviceName, + Method: method, + Payload: payload, + } + resp := &abiv1.BridgeInvokeResponse{} + if err := s.invoke(ctx, "invoke", req, resp); err != nil { + return nil, err + } + return resp.GetPayload(), nil +} + +// Service returns this plugin's own registered service value, for +// HOOK_BRIDGE_CALL dispatch by the wasm shim. +func (s *bridgeStub) Service(serviceName string) (any, bool) { + v, ok := s.services[serviceName] + return v, ok +} diff --git a/plugin/wasmguest/caps/caps.go b/plugin/wasmguest/caps/caps.go new file mode 100644 index 0000000..59bf542 --- /dev/null +++ b/plugin/wasmguest/caps/caps.go @@ -0,0 +1,89 @@ +// Package caps implements every CoreServices capability interface +// (core/plugin/deps.go) as a guest-side stub that marshals to the WO-WZ-001 +// capability messages (abiv1) and dispatches them through a single generic +// guest→host transport. Plugin code keeps compiling and calling against +// content.Content, settings.Settings, plugin.PluginBridge, etc. — unchanged — +// while the concrete work now happens host-side over the wasm ABI. +// +// # The transport seam +// +// A capability call is "marshal a family request, invoke +// '.', unmarshal the reply". That transport is injected as a +// CallFunc so the marshaling logic in this package stays natively testable +// (a fake CallFunc in caps_roundtrip_test.go), while the wasm guest shim +// (package wasmguest, wasip1) binds the real host_call-backed transport. This +// package deliberately does NOT import wasmguest: wasmguest imports caps (to +// assemble the services and reach the guest-side RAG fetcher registry), so the +// dependency only points one way. +// +// # Method disposition +// +// Every CoreServices member is either a stub here or documented host-side in +// core/docs/wasm-abi.md §"Capability calls". Host-side members (no stub): +// Pool (db.* driver), Interceptors (host connect options), AppURL/MediaPath +// (delivered in LoadRequest.host_config), and CoreServiceBindings (static +// manifest.core_service_bindings the host mounts). RAGService. +// RegisterContentFetcher is guest-side (records fetchers for HOOK_RAG_FETCH); +// its Query/OnContentChanged marshal out. +package caps + +import ( + "context" + "errors" + "fmt" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "google.golang.org/protobuf/proto" +) + +// CallFunc is the guest→host capability transport: marshal req, invoke the +// host with method ".", and unmarshal the reply into resp. It +// matches wasmguest.CallHost exactly so the wasip1 shim can bind it directly. +// A nil CallFunc (native builds, DESCRIBE probes) makes every capability call +// fail cleanly with errNoHost rather than panic. +type CallFunc func(method string, req, resp proto.Message) error + +// abiCoded is implemented by transport errors that carry an ABI error code +// (wasmguest.HostError does). It lets the stubs map a DEADLINE_EXCEEDED reply +// onto context.DeadlineExceeded without importing wasmguest. +type abiCoded interface { + AbiErrorCode() abiv1.AbiErrorCode +} + +// base is embedded by every family stub: the family name and the transport. +type base struct { + family string + call CallFunc +} + +// invoke performs one capability call and maps any transport error with +// family/method context. ctx is honored up front — an already-cancelled or +// expired context short-circuits before crossing the boundary — but is not +// forwarded to the transport itself (the host enforces the invoke deadline +// carried in InvokeRequest.deadline_ms; see core/docs/wasm-abi.md). +func (b base) invoke(ctx context.Context, method string, req, resp proto.Message) error { + if b.call == nil { + return fmt.Errorf("%s.%s: no host transport bound (native build)", b.family, method) + } + if ctx != nil { + if err := ctx.Err(); err != nil { + return fmt.Errorf("%s.%s: %w", b.family, method, err) + } + } + return b.mapErr(method, b.call(b.family+"."+method, req, resp)) +} + +// mapErr wraps a transport error with family/method context so plugin logs +// stay legible, and translates an ABI deadline into context.DeadlineExceeded +// so callers' errors.Is(err, context.DeadlineExceeded) keeps working. +func (b base) mapErr(method string, err error) error { + if err == nil { + return nil + } + var coded abiCoded + if errors.As(err, &coded) && + coded.AbiErrorCode() == abiv1.AbiErrorCode_ABI_ERROR_CODE_DEADLINE_EXCEEDED { + return fmt.Errorf("%s.%s: %w: %v", b.family, method, context.DeadlineExceeded, err) + } + return fmt.Errorf("%s.%s: %w", b.family, method, err) +} diff --git a/plugin/wasmguest/caps/caps_roundtrip_test.go b/plugin/wasmguest/caps/caps_roundtrip_test.go new file mode 100644 index 0000000..f4c6f95 --- /dev/null +++ b/plugin/wasmguest/caps/caps_roundtrip_test.go @@ -0,0 +1,957 @@ +package caps + +import ( + "context" + "errors" + "flag" + "os" + "path/filepath" + "strings" + "testing" + "time" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/ai" + "git.dev.alexdunmow.com/block/pluginsdk/content" + "git.dev.alexdunmow.com/block/pluginsdk/gating" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// -update regenerates the golden capability payloads. WO-WZ-006 (cms host) +// replays these SAME goldens to prove the host and guest agree on the wire, so +// they must stay deterministic (proto marshaled with Deterministic=true). +var update = flag.Bool("update", false, "regenerate golden capability payloads") + +const goldenDir = "testdata/golden" + +// Fixed IDs keep the goldens stable across runs. +var ( + idUser = uuid.MustParse("11111111-1111-1111-1111-111111111111") + idAuthor = uuid.MustParse("22222222-2222-2222-2222-222222222222") + idMenu = uuid.MustParse("33333333-3333-3333-3333-333333333333") + idItem = uuid.MustParse("44444444-4444-4444-4444-444444444444") + idParent = uuid.MustParse("55555555-5555-5555-5555-555555555555") + idBucket = uuid.MustParse("66666666-6666-6666-6666-666666666666") + idTier = uuid.MustParse("77777777-7777-7777-7777-777777777777") + idPlan = uuid.MustParse("88888888-8888-8888-8888-888888888888") + idMedia = uuid.MustParse("99999999-9999-9999-9999-999999999999") + idTable = uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + idRow = uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") + planTime = time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC) +) + +func goldenPath(name string) string { return filepath.Join(goldenDir, name+".pb") } + +func detMarshal(t *testing.T, m proto.Message) []byte { + t.Helper() + b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m) + if err != nil { + t.Fatalf("marshal: %v", err) + } + return b +} + +// fakeTransport is the swapped-in CallFunc: it checks the request the stub +// produced against a golden, then feeds back a canned response golden. +type fakeTransport struct { + t *testing.T + name string // golden basename for the current call + cannedResp proto.Message // synthesizes the resp golden under -update + err error // returned instead of a response (error-mapping cases) + gotMethod string +} + +func (f *fakeTransport) call(method string, req, resp proto.Message) error { + f.gotMethod = method + reqBytes := detMarshal(f.t, req) + reqPath := goldenPath(f.name + "_req") + if *update { + writeGolden(f.t, reqPath, reqBytes) + } else { + want, err := os.ReadFile(reqPath) + if err != nil { + f.t.Fatalf("%s: read request golden (run -update?): %v", f.name, err) + } + if !equalProto(f.t, want, reqBytes, req) { + f.t.Errorf("%s: request payload drifted from golden %s", f.name, reqPath) + } + } + if f.err != nil { + return f.err + } + respPath := goldenPath(f.name + "_resp") + if *update { + respBytes := detMarshal(f.t, f.cannedResp) + writeGolden(f.t, respPath, respBytes) + if err := proto.Unmarshal(respBytes, resp); err != nil { + f.t.Fatalf("%s: unmarshal canned resp: %v", f.name, err) + } + return nil + } + respBytes, err := os.ReadFile(respPath) + if err != nil { + f.t.Fatalf("%s: read response golden (run -update?): %v", f.name, err) + } + if err := proto.Unmarshal(respBytes, resp); err != nil { + f.t.Fatalf("%s: unmarshal response golden: %v", f.name, err) + } + return nil +} + +func writeGolden(t *testing.T, path string, b []byte) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir golden: %v", err) + } + if err := os.WriteFile(path, b, 0o644); err != nil { + t.Fatalf("write golden: %v", err) + } +} + +// equalProto compares two serializations by decoding both into fresh messages +// of the same type, so semantically-equal encodings still match. +func equalProto(t *testing.T, want, got []byte, sample proto.Message) bool { + t.Helper() + a := sample.ProtoReflect().New().Interface() + b := sample.ProtoReflect().New().Interface() + if err := proto.Unmarshal(want, a); err != nil { + t.Fatalf("unmarshal want: %v", err) + } + if err := proto.Unmarshal(got, b); err != nil { + t.Fatalf("unmarshal got: %v", err) + } + return proto.Equal(a, b) +} + +// capCase is one capability round trip: set up the fake, call the stub through +// the assembled CoreServices, and assert both the wire method and the decoded +// Go result. +type capCase struct { + name string + wantMethod string + resp proto.Message + run func(t *testing.T, cs plugin.CoreServices) +} + +func TestCapabilityRoundTrip(t *testing.T) { + ctx := context.Background() + f := &fakeTransport{t: t} + cs := NewCoreServices(f.call) + + cases := []capCase{ + // --- content --- + { + name: "content_get_author_profile", wantMethod: "content.get_author_profile", + resp: &abiv1.ContentGetAuthorProfileResponse{Author: &abiv1.AuthorProfile{ + Id: idAuthor.String(), Name: "Ada", Slug: "ada", Bio: "hi", + AvatarUrl: "https://x/a.png", Website: "https://ada.dev", + SocialLinks: map[string]string{"x": "@ada", "gh": "ada"}, + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Content.GetAuthorProfile(ctx, idAuthor) + if err != nil { + t.Fatal(err) + } + if got.ID != idAuthor || got.Name != "Ada" || got.SocialLinks["x"] != "@ada" { + t.Errorf("author = %+v", got) + } + }, + }, + { + name: "content_get_page", wantMethod: "content.get_page", + resp: &abiv1.ContentGetPageResponse{Page: &abiv1.PageInfo{Id: idAuthor.String(), Slug: "about", Title: "About Us"}}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Content.GetPage(ctx, "about") + if err != nil { + t.Fatal(err) + } + if got.Slug != "about" || got.Title != "About Us" { + t.Errorf("page = %+v", got) + } + }, + }, + { + name: "content_get_post", wantMethod: "content.get_post", + resp: &abiv1.ContentGetPostResponse{Post: &abiv1.PostInfo{ + Id: idAuthor.String(), Slug: "hello", Title: "Hello", Excerpt: "hi", + FeaturedImageUrl: "media:x", AuthorId: idAuthor.String(), + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Content.GetPost(ctx, "hello") + if err != nil { + t.Fatal(err) + } + if got.Title != "Hello" || got.AuthorID != idAuthor { + t.Errorf("post = %+v", got) + } + }, + }, + { + name: "content_list_posts", wantMethod: "content.list_posts", + resp: &abiv1.ContentListPostsResponse{Posts: []*abiv1.PostInfo{ + { + Id: idAuthor.String(), Slug: "first", Title: "First", Excerpt: "one", + FeaturedImageUrl: "media:1", AuthorId: idAuthor.String(), + AuthorName: "Ada", AuthorSlug: "ada", PublishedAt: timestamppb.New(planTime), + }, + { + Id: idPlan.String(), Slug: "second", Title: "Second", Excerpt: "two", + AuthorId: idAuthor.String(), AuthorName: "Ada", AuthorSlug: "ada", + PublishedAt: timestamppb.New(planTime), + }, + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Content.ListPosts(ctx, content.ListPostsParams{Limit: 10, IncludeExcerpt: true}) + if err != nil { + t.Fatal(err) + } + if len(got) != 2 || got[0].Slug != "first" || got[0].AuthorName != "Ada" { + t.Fatalf("posts = %+v", got) + } + if got[1].Slug != "second" || !got[1].PublishedAt.Equal(planTime) { + t.Errorf("post[1] = %+v", got[1]) + } + }, + }, + { + name: "content_slugify", wantMethod: "content.slugify", + resp: &abiv1.ContentSlugifyResponse{Slug: "hello-world"}, + run: func(t *testing.T, cs plugin.CoreServices) { + if got := cs.Content.Slugify("Hello World"); got != "hello-world" { + t.Errorf("slug = %q", got) + } + }, + }, + { + name: "content_block_note_to_html", wantMethod: "content.block_note_to_html", + resp: &abiv1.ContentBlockNoteToHtmlResponse{Html: "

hi

"}, + run: func(t *testing.T, cs plugin.CoreServices) { + if got := cs.Content.BlockNoteToHTML(ctx, map[string]any{"type": "doc"}); got != "

hi

" { + t.Errorf("html = %q", got) + } + }, + }, + { + name: "content_generate_excerpt", wantMethod: "content.generate_excerpt", + resp: &abiv1.ContentGenerateExcerptResponse{Excerpt: "short"}, + run: func(t *testing.T, cs plugin.CoreServices) { + if got := cs.Content.GenerateExcerpt("

long text

", 5); got != "short" { + t.Errorf("excerpt = %q", got) + } + }, + }, + { + name: "content_strip_html", wantMethod: "content.strip_html", + resp: &abiv1.ContentStripHtmlResponse{Text: "plain"}, + run: func(t *testing.T, cs plugin.CoreServices) { + if got := cs.Content.StripHTML("plain"); got != "plain" { + t.Errorf("text = %q", got) + } + }, + }, + // --- settings --- + { + name: "settings_get_site_settings", wantMethod: "settings.get_site_settings", + resp: &abiv1.SettingsGetSiteSettingsResponse{SettingsJson: []byte(`{"site_name":"Fixture","theme":"dark"}`)}, + run: func(t *testing.T, cs plugin.CoreServices) { + m, err := cs.Settings.GetSiteSettings(ctx) + if err != nil { + t.Fatal(err) + } + if m["site_name"] != "Fixture" { + t.Errorf("settings = %+v", m) + } + }, + }, + { + name: "settings_get_plugin_settings", wantMethod: "settings.get_plugin_settings", + resp: &abiv1.SettingsGetPluginSettingsResponse{SettingsJson: []byte(`{"enabled":true}`)}, + run: func(t *testing.T, cs plugin.CoreServices) { + m, err := cs.Settings.GetPluginSettings(ctx, "symposium") + if err != nil { + t.Fatal(err) + } + if m["enabled"] != true { + t.Errorf("plugin settings = %+v", m) + } + }, + }, + { + name: "settings_update_site_setting", wantMethod: "settings.update_site_setting", + resp: &abiv1.SettingsUpdateSiteSettingResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.SettingsUpdater.UpdateSiteSetting(ctx, "theme", "light"); err != nil { + t.Fatal(err) + } + }, + }, + // --- gating --- + { + name: "gating_get_subscriber_tier_level", wantMethod: "gating.get_subscriber_tier_level", + resp: &abiv1.GatingGetSubscriberTierLevelResponse{Level: 3}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Gating.GetSubscriberTierLevel(ctx, idUser) + if err != nil { + t.Fatal(err) + } + if got != 3 { + t.Errorf("level = %d", got) + } + }, + }, + { + name: "gating_evaluate_access", wantMethod: "gating.evaluate_access", + resp: &abiv1.GatingEvaluateAccessResponse{Result: &abiv1.AccessResult{ + HasAccess: false, TeaserMode: "soft", TeaserPercent: 20, RequiredLevel: 2, + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got := cs.Gating.EvaluateAccess(1, &gating.AccessRule{MinTierLevel: 2, TeaserMode: "soft", TeaserPercent: 20}) + if got.HasAccess || got.TeaserMode != "soft" || got.RequiredLevel != 2 { + t.Errorf("access = %+v", got) + } + }, + }, + // --- crypto --- + { + name: "crypto_encrypt_secret", wantMethod: "crypto.encrypt_secret", + resp: &abiv1.CryptoEncryptSecretResponse{Ciphertext: "enc:abc"}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Crypto.EncryptSecret("plain") + if err != nil || got != "enc:abc" { + t.Errorf("ciphertext = %q err = %v", got, err) + } + }, + }, + { + name: "crypto_decrypt_secret", wantMethod: "crypto.decrypt_secret", + resp: &abiv1.CryptoDecryptSecretResponse{Plaintext: "plain"}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Crypto.DecryptSecret("enc:abc") + if err != nil || got != "plain" { + t.Errorf("plaintext = %q err = %v", got, err) + } + }, + }, + // --- menus --- + { + name: "menus_get_menu_by_name", wantMethod: "menus.get_menu_by_name", + resp: &abiv1.MenusGetMenuByNameResponse{Menu: &abiv1.Menu{Id: idMenu.String(), Name: "main"}}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Menus.GetMenuByName(ctx, "main") + if err != nil || got.ID != idMenu || got.Name != "main" { + t.Errorf("menu = %+v err = %v", got, err) + } + }, + }, + { + name: "menus_get_menu_items", wantMethod: "menus.get_menu_items", + resp: &abiv1.MenusGetMenuItemsResponse{Items: []*abiv1.MenuItem{{ + Id: idItem.String(), MenuId: idMenu.String(), Label: "Home", Url: "/", + PageSlug: "home", ParentId: new(idParent.String()), SortOrder: 1, + OpenInNewTab: true, CssClass: "nav", ItemType: "link", Icon: "home", + }}}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Menus.GetMenuItems(ctx, idMenu) + if err != nil || len(got) != 1 { + t.Fatalf("items = %+v err = %v", got, err) + } + it := got[0] + if it.ID != idItem || it.ParentID == nil || *it.ParentID != idParent || !it.OpenInNewTab { + t.Errorf("item = %+v", it) + } + }, + }, + // --- datasources --- + { + name: "datasources_resolve_bucket", wantMethod: "datasources.resolve_bucket", + resp: &abiv1.DatasourcesResolveBucketResponse{Result: &abiv1.DatasourceResult{ + ItemsJson: []byte(`[{"id":1},{"id":2}]`), Total: 2, MetaJson: []byte(`{"page":1}`), + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Datasources.ResolveBucket(ctx, idBucket) + if err != nil || got.Total != 2 || len(got.Items) != 2 || got.Meta["page"] != float64(1) { + t.Errorf("result = %+v err = %v", got, err) + } + }, + }, + { + name: "datasources_resolve_bucket_by_key", wantMethod: "datasources.resolve_bucket_by_key", + resp: &abiv1.DatasourcesResolveBucketByKeyResponse{Result: &abiv1.DatasourceResult{ + ItemsJson: []byte(`[]`), Total: 0, + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Datasources.ResolveBucketByKey(ctx, "featured") + if err != nil || got.Total != 0 { + t.Errorf("result = %+v err = %v", got, err) + } + }, + }, + // --- users --- + { + name: "users_get_by_username", wantMethod: "users.get_by_username", + resp: &abiv1.UsersGetByUsernameResponse{User: &abiv1.PublicUserProfile{ + Id: idUser.String(), Email: "u@x.com", Username: "u", DisplayName: "U", + AvatarUrl: "a", Bio: "b", EmailVerified: true, Role: "member", + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.PublicUsers.GetByUsername(ctx, "u") + if err != nil || got.ID != idUser || !got.EmailVerified { + t.Errorf("user = %+v err = %v", got, err) + } + }, + }, + { + name: "users_get_by_id", wantMethod: "users.get_by_id", + resp: &abiv1.UsersGetByIdResponse{User: &abiv1.PublicUserProfile{Id: idUser.String(), Username: "u"}}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.PublicUsers.GetByID(ctx, idUser) + if err != nil || got.Username != "u" { + t.Errorf("user = %+v err = %v", got, err) + } + }, + }, + // --- subscriptions --- + { + name: "subscriptions_get_user_tier_level", wantMethod: "subscriptions.get_user_tier_level", + resp: &abiv1.SubscriptionsGetUserTierLevelResponse{TierLevel: &abiv1.TierLevel{Level: 5, Features: []byte(`{"a":1}`)}}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Subscriptions.GetUserTierLevel(ctx, idUser) + if err != nil || got.Level != 5 || string(got.Features) != `{"a":1}` { + t.Errorf("tier level = %+v err = %v", got, err) + } + }, + }, + { + name: "subscriptions_get_tier_by_slug", wantMethod: "subscriptions.get_tier_by_slug", + resp: &abiv1.SubscriptionsGetTierBySlugResponse{Tier: &abiv1.Tier{ + Id: idTier.String(), Name: "Gold", Slug: "gold", Level: 3, Description: "d", + Features: []byte(`{}`), IsDefault: true, Position: 2, + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Subscriptions.GetTierBySlug(ctx, "gold") + if err != nil || got.ID != idTier || got.Level != 3 || !got.IsDefault { + t.Errorf("tier = %+v err = %v", got, err) + } + }, + }, + { + name: "subscriptions_list_tiers", wantMethod: "subscriptions.list_tiers", + resp: &abiv1.SubscriptionsListTiersResponse{Tiers: []*abiv1.Tier{ + {Id: idTier.String(), Name: "Gold", Slug: "gold", Level: 3}, + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Subscriptions.ListTiers(ctx) + if err != nil || len(got) != 1 || got[0].Slug != "gold" { + t.Errorf("tiers = %+v err = %v", got, err) + } + }, + }, + { + name: "subscriptions_list_active_plans", wantMethod: "subscriptions.list_active_plans", + resp: &abiv1.SubscriptionsListActivePlansResponse{Plans: []*abiv1.Plan{{ + Id: idPlan.String(), TierId: idTier.String(), BillingInterval: "month", + Amount: 4900, Currency: "usd", IsActive: true, CreatedAt: timestamppb.New(planTime), + }}}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Subscriptions.ListActivePlans(ctx, idTier) + if err != nil || len(got) != 1 { + t.Fatalf("plans = %+v err = %v", got, err) + } + p := got[0] + if p.ID != idPlan || p.Amount != 4900 || !p.CreatedAt.Equal(planTime) { + t.Errorf("plan = %+v", p) + } + }, + }, + // --- media --- + { + name: "media_deposit", wantMethod: "media.deposit", + resp: &abiv1.MediaDepositResponse{Id: idMedia.String(), Ref: "media:" + idMedia.String(), Created: true}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Media.Deposit(ctx, plugin.MediaDeposit{ + ID: idMedia, Filename: "hero.jpg", Data: []byte("bytes"), AltText: "alt", Folder: "f", Source: "plugin", + }) + if err != nil || got.ID != idMedia || !got.Created { + t.Errorf("media = %+v err = %v", got, err) + } + }, + }, + // --- email --- + { + name: "email_send", wantMethod: "email.send", + resp: &abiv1.EmailSendResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.EmailSender.Send("to@x.com", "Subj", "Body"); err != nil { + t.Fatal(err) + } + }, + }, + // --- ai --- + { + name: "ai_text_call", wantMethod: "ai.text_call", + resp: &abiv1.AiTextCallResponse{Text: "generated"}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.AITextCall(ctx, "summarize", "sys", "user") + if err != nil || got != "generated" { + t.Errorf("text = %q err = %v", got, err) + } + }, + }, + { + name: "ai_tools_register", wantMethod: "ai.tools.register", + resp: &abiv1.AiToolRegisterResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + cs.ToolRegistry.Register(&ai.ToolDefinition{ + Slug: "lookup", Name: "Lookup", Description: "d", + ParameterSchema: map[string]any{"type": "object"}, + }) + }, + }, + // --- bridge --- + { + name: "bridge_register_service", wantMethod: "bridge.register_service", + resp: &abiv1.BridgeRegisterServiceResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + cs.Bridge.RegisterService("symposium", "search", struct{}{}) + }, + }, + { + name: "bridge_get_service", wantMethod: "bridge.get_service", + resp: &abiv1.BridgeGetServiceResponse{Available: true}, + run: func(t *testing.T, cs plugin.CoreServices) { + if got := cs.Bridge.GetService("symposium", "search"); got != nil { + t.Errorf("get_service crossed a value: %v (want nil)", got) + } + }, + }, + // --- jobs --- + { + name: "jobs_submit", wantMethod: "jobs.submit", + resp: &abiv1.JobsSubmitResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.JobRunner.Submit(ctx, "reindex", []byte(`{"full":true}`)); err != nil { + t.Fatal(err) + } + }, + }, + // --- embeddings --- + { + name: "embeddings_generate_embedding", wantMethod: "embeddings.generate_embedding", + resp: &abiv1.EmbeddingsGenerateEmbeddingResponse{Embedding: []float32{0.1, 0.2, 0.3}}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.EmbeddingService.GenerateEmbedding(ctx, "text") + if err != nil || len(got) != 3 || got[0] != 0.1 { + t.Errorf("embedding = %v err = %v", got, err) + } + }, + }, + { + name: "embeddings_embed_content", wantMethod: "embeddings.embed_content", + resp: &abiv1.EmbeddingsEmbedContentResponse{Embedded: true}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.EmbeddingService.EmbedContent(ctx, "post", idRow, "text") + if err != nil || !got { + t.Errorf("embedded = %v err = %v", got, err) + } + }, + }, + { + name: "embeddings_is_available", wantMethod: "embeddings.is_available", + resp: &abiv1.EmbeddingsIsAvailableResponse{Available: true}, + run: func(t *testing.T, cs plugin.CoreServices) { + if !cs.EmbeddingService.IsAvailable() { + t.Errorf("is_available = false") + } + }, + }, + // --- rag --- + { + name: "rag_query", wantMethod: "rag.query", + resp: &abiv1.RagQueryResponse{Results: []*abiv1.RagResult{ + {Content: "chunk", Score: 0.9, Metadata: map[string]string{"src": "post"}}, + }}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.RAGService.Query(ctx, "q", 5) + if err != nil || len(got) != 1 || got[0].Score != 0.9 || got[0].Metadata["src"] != "post" { + t.Errorf("rag = %+v err = %v", got, err) + } + }, + }, + { + name: "rag_on_content_changed", wantMethod: "rag.on_content_changed", + resp: &abiv1.RagOnContentChangedResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + cs.RAGService.OnContentChanged(ctx, "post", idRow) + }, + }, + // --- reviews --- + { + name: "reviews_submit_review", wantMethod: "reviews.submit_review", + resp: &abiv1.ReviewsSubmitReviewResponse{ReviewId: "rev-1"}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.ReviewSubmitter.SubmitReview(ctx, plugin.SubmitReviewParams{ + TableID: idTable, RowID: idRow, OverallRating: 4, ReviewText: "good", + Ratings: map[string]any{"food": 5}, Photos: []string{"media:1"}, + }) + if err != nil || got != "rev-1" { + t.Errorf("review id = %q err = %v", got, err) + } + }, + }, + // --- badges --- + { + name: "badges_refresh_badges", wantMethod: "badges.refresh_badges", + resp: &abiv1.BadgesRefreshBadgesResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.BadgeRefresher.RefreshBadges(ctx, idTable, idRow); err != nil { + t.Fatal(err) + } + }, + }, + // --- content writes (WO-WZ-019) --- + { + name: "content_create_page", wantMethod: "content.create_page", + resp: &abiv1.ContentCreatePageResponse{PageId: idItem.String(), Created: true}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.ContentAuthor.CreatePage(ctx, content.CreatePageParams{ + Slug: "/pricing", ParentSlug: "", Title: "Pricing", TemplateKey: "landing", + }) + if err != nil || got.PageID != idItem || !got.Created { + t.Errorf("create_page = %+v err = %v", got, err) + } + }, + }, + { + name: "content_set_page_blocks", wantMethod: "content.set_page_blocks", + resp: &abiv1.ContentSetPageBlocksResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + html := "

Hi

" + err := cs.ContentAuthor.SetPageBlocks(ctx, idItem, []content.PageBlock{ + {BlockKey: "html", Title: "Hero", Content: map[string]any{"align": "center"}, HTMLContent: &html, Slot: "main", SortOrder: 1}, + {BlockKey: "cta", Title: "CTA", Content: map[string]any{"label": "Go"}, SortOrder: 2}, + }) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "content_publish_page", wantMethod: "content.publish_page", + resp: &abiv1.ContentPublishPageResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.ContentAuthor.PublishPage(ctx, idItem); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "content_set_page_seo", wantMethod: "content.set_page_seo", + resp: &abiv1.ContentSetPageSeoResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + mt, md := "Pricing — Acme", "Plans and pricing." + err := cs.ContentAuthor.SetPageSEO(ctx, idItem, content.PageSEO{MetaTitle: &mt, MetaDescription: &md}) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "content_upsert_post", wantMethod: "content.upsert_post", + resp: &abiv1.ContentUpsertPostResponse{PostId: idRow.String(), Created: false}, + run: func(t *testing.T, cs plugin.CoreServices) { + excerpt := "hello" + got, err := cs.ContentAuthor.UpsertPost(ctx, content.UpsertPostParams{ + Slug: "hello", Title: "Hello", Document: map[string]any{"type": "doc"}, + Excerpt: &excerpt, AuthorProfileID: &idAuthor, FeaturedImageID: &idMedia, + Publish: true, + }) + if err != nil || got.PostID != idRow || got.Created { + t.Errorf("upsert_post = %+v err = %v", got, err) + } + }, + }, + // --- settings.update_plugin_settings (WO-WZ-019) --- + { + name: "settings_update_plugin_settings", wantMethod: "settings.update_plugin_settings", + resp: &abiv1.SettingsUpdatePluginSettingsResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.SettingsUpdater.UpdatePluginSettings(ctx, "myplugin", map[string]any{"enabled": true}) + if err != nil { + t.Fatal(err) + } + }, + }, + // --- provisioner (WO-WZ-019) --- + { + name: "provisioner_ensure_page", wantMethod: "provisioner.ensure_page", + resp: &abiv1.ProvisionerEnsurePageResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.EnsurePage(plugin.PageConfig{ + Slug: "/", Title: "Home", TemplateKey: "homepage", ReconcileBlocks: true, + Blocks: []plugin.PageBlockConfig{ + {BlockKey: "html", Title: "Hero", Content: map[string]any{"x": float64(1)}, Slot: "main", SortOrder: 1}, + }, + }) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_merge_site_settings", wantMethod: "provisioner.merge_site_settings", + resp: &abiv1.ProvisionerMergeSiteSettingsResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.Provisioner.MergeSiteSettings(map[string]any{"site_name": "Acme"}); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_ensure_menu_item", wantMethod: "provisioner.ensure_menu_item", + resp: &abiv1.ProvisionerEnsureMenuItemResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{Label: "Docs", PageSlug: "/docs", SortOrder: 3}) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_ensure_media", wantMethod: "provisioner.ensure_media", + resp: &abiv1.ProvisionerEnsureMediaResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.EnsureMedia(plugin.MediaDeposit{ + ID: idMedia, Filename: "hero.jpg", Data: []byte{1, 2, 3}, AltText: "hero", Folder: "seed", Source: "myplugin", + }) + if err != nil { + t.Fatal(err) + } + }, + }, + // --- bridge.invoke (WO-WZ-019) --- + { + name: "bridge_invoke", wantMethod: "bridge.invoke", + resp: &abiv1.BridgeInvokeResponse{Payload: []byte(`{"ok":true}`)}, + run: func(t *testing.T, cs plugin.CoreServices) { + got, err := cs.Bridge.Invoke(ctx, "symposium", "seeder", "seed_forum", []byte(`{"n":1}`)) + if err != nil || string(got) != `{"ok":true}` { + t.Errorf("bridge.invoke = %q err = %v", got, err) + } + }, + }, + // --- remaining provisioner methods (WO-WZ-019) --- + { + name: "provisioner_ensure_data_table", wantMethod: "provisioner.ensure_data_table", + resp: &abiv1.ProvisionerEnsureDataTableResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.EnsureDataTable(plugin.DataTableConfig{ + Key: "playgrounds", Name: "Playgrounds", Description: "d", + Schema: []byte(`{"fields":[]}`), PrimaryKey: "id", + }) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_ensure_setting", wantMethod: "provisioner.ensure_setting", + resp: &abiv1.ProvisionerEnsureSettingResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.Provisioner.EnsureSetting("review_dimensions", map[string]any{"max": float64(5)}); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_override_site_settings", wantMethod: "provisioner.override_site_settings", + resp: &abiv1.ProvisionerOverrideSiteSettingsResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.Provisioner.OverrideSiteSettings(map[string]any{"default_template": "homepage"}); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_register_embedding_config", wantMethod: "provisioner.register_embedding_config", + resp: &abiv1.ProvisionerRegisterEmbeddingConfigResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.RegisterEmbeddingConfig(plugin.EmbeddingConfigDef{ + TableKey: "playgrounds", TextTemplate: "{{name}}", Enabled: true, + }) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_ensure_embed", wantMethod: "provisioner.ensure_embed", + resp: &abiv1.ProvisionerEnsureEmbedResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.EnsureEmbed(plugin.EmbedConfig{ + Key: "playground-card", Title: "Playground Card", Description: "d", + Icon: "📍", LabelField: "name", Template: "
{{name}}
", + DataSource: plugin.EmbedDataSource{Type: "row", TableKey: "playgrounds"}, + }) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_ensure_job_schedule", wantMethod: "provisioner.ensure_job_schedule", + resp: &abiv1.ProvisionerEnsureJobScheduleResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.EnsureJobSchedule(plugin.JobScheduleConfig{ + JobType: "bounty_rotation", CronExpression: "0 3 * * *", Config: []byte(`{"n":3}`), + }) + if err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_update_data_table_row_field", wantMethod: "provisioner.update_data_table_row_field", + resp: &abiv1.ProvisionerUpdateDataTableRowFieldResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.Provisioner.UpdateDataTableRowField(ctx, idRow, "status", "active"); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_disable_orphaned_job_schedules", wantMethod: "provisioner.disable_orphaned_job_schedules", + resp: &abiv1.ProvisionerDisableOrphanedJobSchedulesResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.Provisioner.DisableOrphanedJobSchedules([]string{"bounty_rotation", "reindex"}); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_ensure_plugin", wantMethod: "provisioner.ensure_plugin", + resp: &abiv1.ProvisionerEnsurePluginResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + if err := cs.Provisioner.EnsurePlugin("symposium"); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "provisioner_ensure_custom_color", wantMethod: "provisioner.ensure_custom_color", + resp: &abiv1.ProvisionerEnsureCustomColorResponse{}, + run: func(t *testing.T, cs plugin.CoreServices) { + err := cs.Provisioner.EnsureCustomColor(plugin.CustomColorConfig{ + Name: "brand", LightValue: "oklch(0.7 0.1 200)", DarkValue: "oklch(0.4 0.1 200)", Source: "myplugin", + }) + if err != nil { + t.Fatal(err) + } + }, + }, + // --- jobs.progress (WO-WZ-019; sent by the job dispatch path, not a + // CoreServices stub, so the case drives the transport directly) --- + { + name: "jobs_progress", wantMethod: "jobs.progress", + resp: &abiv1.JobsProgressResponse{}, + run: func(t *testing.T, _ plugin.CoreServices) { + req := &abiv1.JobsProgressRequest{Current: 2, Total: 5, Message: "halfway"} + if err := f.call("jobs.progress", req, &abiv1.JobsProgressResponse{}); err != nil { + t.Fatal(err) + } + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + f.name = tc.name + f.cannedResp = tc.resp + f.err = nil + f.gotMethod = "" + tc.run(t, cs) + if f.gotMethod != tc.wantMethod { + t.Errorf("method = %q, want %q", f.gotMethod, tc.wantMethod) + } + }) + } +} + +// fakeAbiErr is a transport error carrying an ABI code, like wasmguest.HostError. +type fakeAbiErr struct { + code abiv1.AbiErrorCode + msg string +} + +func (e *fakeAbiErr) Error() string { return e.msg } +func (e *fakeAbiErr) AbiErrorCode() abiv1.AbiErrorCode { return e.code } + +func TestErrorMappingCarriesFamilyMethod(t *testing.T) { + f := &fakeTransport{t: t, err: &fakeAbiErr{code: abiv1.AbiErrorCode_ABI_ERROR_CODE_PERMISSION_DENIED, msg: "denied"}} + cs := NewCoreServices(f.call) + f.name = "content_get_page" // reuse the request golden for the compare + + _, err := cs.Content.GetPage(context.Background(), "about") + if err == nil { + t.Fatal("want error") + } + if got := err.Error(); !strings.Contains(got, "content.get_page") || !strings.Contains(got, "denied") { + t.Errorf("error %q lacks family/method or message", got) + } +} + +func TestErrorMappingDeadlineBecomesContextDeadline(t *testing.T) { + f := &fakeTransport{t: t, err: &fakeAbiErr{code: abiv1.AbiErrorCode_ABI_ERROR_CODE_DEADLINE_EXCEEDED, msg: "deadline"}} + cs := NewCoreServices(f.call) + f.name = "crypto_encrypt_secret" + + _, err := cs.Crypto.EncryptSecret("plain") + if err == nil { + t.Fatal("want error") + } + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("error %q does not wrap context.DeadlineExceeded", err) + } + if !strings.Contains(err.Error(), "crypto.encrypt_secret") { + t.Errorf("error %q lacks family/method", err) + } +} + +func TestNilTransportFailsCleanly(t *testing.T) { + cs := NewCoreServices(nil) + _, err := cs.Content.GetPage(context.Background(), "about") + if err == nil || !strings.Contains(err.Error(), "no host transport") { + t.Errorf("nil transport error = %v", err) + } +} + +// TestRAGRegisterContentFetcherStaysGuestSide verifies RegisterContentFetcher +// records fetchers locally (no host call) for HOOK_RAG_FETCH dispatch. +func TestRAGRegisterContentFetcherStaysGuestSide(t *testing.T) { + f := &fakeTransport{t: t} + cs := NewCoreServices(f.call) + rag, ok := cs.RAGService.(*RAGStub) + if !ok { + t.Fatalf("RAGService is %T, want *RAGStub", cs.RAGService) + } + called := false + rag.RegisterContentFetcher("post", func(context.Context, uuid.UUID) (string, string, error) { + called = true + return "T", "body", nil + }) + if f.gotMethod != "" { + t.Errorf("RegisterContentFetcher made a host call %q", f.gotMethod) + } + if types := rag.FetcherTypes(); len(types) != 1 || types[0] != "post" { + t.Errorf("fetcher types = %v", types) + } + fetcher, ok := rag.Fetcher("post") + if !ok { + t.Fatal("fetcher not found") + } + if _, _, _ = fetcher(context.Background(), idRow); !called { + t.Error("registered fetcher not invoked") + } +} diff --git a/plugin/wasmguest/caps/content.go b/plugin/wasmguest/caps/content.go new file mode 100644 index 0000000..8297d1e --- /dev/null +++ b/plugin/wasmguest/caps/content.go @@ -0,0 +1,141 @@ +package caps + +import ( + "context" + "encoding/json" + "time" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/content" + "github.com/google/uuid" +) + +// contentStub implements content.Content over content.* capability calls. +type contentStub struct{ base } + +var _ content.Content = (*contentStub)(nil) + +func (s *contentStub) GetAuthorProfile(ctx context.Context, id uuid.UUID) (*content.AuthorProfile, error) { + resp := &abiv1.ContentGetAuthorProfileResponse{} + if err := s.invoke(ctx, "get_author_profile", &abiv1.ContentGetAuthorProfileRequest{Id: id.String()}, resp); err != nil { + return nil, err + } + a := resp.GetAuthor() + if a == nil { + return nil, nil + } + return &content.AuthorProfile{ + ID: parseUUID(a.GetId()), + Name: a.GetName(), + Slug: a.GetSlug(), + Bio: a.GetBio(), + AvatarURL: a.GetAvatarUrl(), + Website: a.GetWebsite(), + SocialLinks: a.GetSocialLinks(), + }, nil +} + +func (s *contentStub) GetPage(ctx context.Context, slug string) (*content.PageInfo, error) { + resp := &abiv1.ContentGetPageResponse{} + if err := s.invoke(ctx, "get_page", &abiv1.ContentGetPageRequest{Slug: slug}, resp); err != nil { + return nil, err + } + p := resp.GetPage() + if p == nil { + return nil, nil + } + return &content.PageInfo{ID: parseUUID(p.GetId()), Slug: p.GetSlug(), Title: p.GetTitle()}, nil +} + +func (s *contentStub) GetPost(ctx context.Context, slug string) (*content.PostInfo, error) { + resp := &abiv1.ContentGetPostResponse{} + if err := s.invoke(ctx, "get_post", &abiv1.ContentGetPostRequest{Slug: slug}, resp); err != nil { + return nil, err + } + p := resp.GetPost() + if p == nil { + return nil, nil + } + info := postInfoFromProto(p) + return &info, nil +} + +func (s *contentStub) ListPosts(ctx context.Context, params content.ListPostsParams) ([]content.PostInfo, error) { + req := &abiv1.ContentListPostsRequest{ + Limit: int32(params.Limit), + Offset: int32(params.Offset), + Category: params.Category, + PublishedOnly: params.PublishedOnly, + IncludeBody: params.IncludeBody, + IncludeExcerpt: params.IncludeExcerpt, + } + resp := &abiv1.ContentListPostsResponse{} + if err := s.invoke(ctx, "list_posts", req, resp); err != nil { + return nil, err + } + out := make([]content.PostInfo, 0, len(resp.GetPosts())) + for _, p := range resp.GetPosts() { + out = append(out, postInfoFromProto(p)) + } + return out, nil +} + +// postInfoFromProto decodes an abiv1.PostInfo into the SDK content.PostInfo, +// shared by GetPost and ListPosts. +func postInfoFromProto(p *abiv1.PostInfo) content.PostInfo { + var publishedAt time.Time + if ts := p.GetPublishedAt(); ts != nil { + publishedAt = ts.AsTime() + } + return content.PostInfo{ + ID: parseUUID(p.GetId()), + Slug: p.GetSlug(), + Title: p.GetTitle(), + Excerpt: p.GetExcerpt(), + Body: p.GetBody(), + FeaturedImageURL: p.GetFeaturedImageUrl(), + AuthorID: parseUUID(p.GetAuthorId()), + AuthorName: p.GetAuthorName(), + AuthorSlug: p.GetAuthorSlug(), + PublishedAt: publishedAt, + } +} + +// Slugify has no error channel in the interface; a transport failure degrades +// to the empty string (the caller treats an empty slug as "unavailable"). +func (s *contentStub) Slugify(text string) string { + resp := &abiv1.ContentSlugifyResponse{} + if err := s.invoke(context.Background(), "slugify", &abiv1.ContentSlugifyRequest{Text: text}, resp); err != nil { + return "" + } + return resp.GetSlug() +} + +func (s *contentStub) BlockNoteToHTML(ctx context.Context, doc map[string]any) string { + docJSON, err := json.Marshal(doc) + if err != nil { + return "" + } + resp := &abiv1.ContentBlockNoteToHtmlResponse{} + if err := s.invoke(ctx, "block_note_to_html", &abiv1.ContentBlockNoteToHtmlRequest{DocJson: docJSON}, resp); err != nil { + return "" + } + return resp.GetHtml() +} + +func (s *contentStub) GenerateExcerpt(html string, maxLen int) string { + resp := &abiv1.ContentGenerateExcerptResponse{} + req := &abiv1.ContentGenerateExcerptRequest{Html: html, MaxLen: int32(maxLen)} + if err := s.invoke(context.Background(), "generate_excerpt", req, resp); err != nil { + return "" + } + return resp.GetExcerpt() +} + +func (s *contentStub) StripHTML(str string) string { + resp := &abiv1.ContentStripHtmlResponse{} + if err := s.invoke(context.Background(), "strip_html", &abiv1.ContentStripHtmlRequest{Html: str}, resp); err != nil { + return "" + } + return resp.GetText() +} diff --git a/plugin/wasmguest/caps/coreservices.go b/plugin/wasmguest/caps/coreservices.go new file mode 100644 index 0000000..4ef9034 --- /dev/null +++ b/plugin/wasmguest/caps/coreservices.go @@ -0,0 +1,51 @@ +package caps + +import ( + "git.dev.alexdunmow.com/block/pluginsdk/plugin" +) + +// NewCoreServices assembles the guest-side CoreServices value plugins receive +// in their Load/HTTP/Job hooks: every capability interface is a stub that +// marshals to a "." host call over the injected transport. +// +// The transport is injected (not a package global) so the marshaling logic is +// natively testable with a fake CallFunc; the wasm shim (package wasmguest, +// wasip1) passes its host_call-backed CallHost. A nil call yields stubs that +// fail every capability with a clear "no host transport" error — the shape +// used by DESCRIBE probes, which never reach a live host. +// +// Members intentionally left zero because they do NOT cross as capability +// calls (all documented in core/docs/wasm-abi.md §"Capability calls"): +// +// - Pool → the db.* driver messages (db.proto) +// - Interceptors → host-side connect options; RBAC from the manifest +// - MediaPath / AppURL → delivered in LoadRequest.host_config at load +// - CoreServiceBindings → static manifest.core_service_bindings; host mounts +func NewCoreServices(call CallFunc) plugin.CoreServices { + settings := &settingsStub{base: base{family: "settings", call: call}} + ai := &aiStub{base: base{family: "ai", call: call}} + + return plugin.CoreServices{ + Content: &contentStub{base{family: "content", call: call}}, + ContentAuthor: &authorStub{base{family: "content", call: call}}, + Settings: settings, + SettingsUpdater: settings, + Gating: &gatingStub{base{family: "gating", call: call}}, + Crypto: &cryptoStub{base{family: "crypto", call: call}}, + Menus: &menusStub{base{family: "menus", call: call}}, + Datasources: &datasourcesStub{base{family: "datasources", call: call}}, + PublicUsers: &usersStub{base{family: "users", call: call}}, + Subscriptions: &subscriptionsStub{base{family: "subscriptions", call: call}}, + Media: &mediaStub{base{family: "media", call: call}}, + ToolRegistry: ai, + AITextCall: ai.textCall, + EmailSender: &emailStub{base{family: "email", call: call}}, + Bridge: &bridgeStub{base: base{family: "bridge", call: call}}, + ReviewSubmitter: &reviewsStub{base{family: "reviews", call: call}}, + BadgeRefresher: &badgesStub{base{family: "badges", call: call}}, + JobRunner: &jobsStub{base{family: "jobs", call: call}}, + EmbeddingService: &embeddingsStub{base{family: "embeddings", call: call}}, + RAGService: NewRAGStub(call), + Provisioner: &provisionerStub{base{family: "provisioner", call: call}}, + } +} diff --git a/plugin/wasmguest/caps/crypto.go b/plugin/wasmguest/caps/crypto.go new file mode 100644 index 0000000..8c2bac0 --- /dev/null +++ b/plugin/wasmguest/caps/crypto.go @@ -0,0 +1,30 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/crypto" +) + +// cryptoStub implements crypto.Crypto over crypto.* capability calls. The +// interface takes no context; host calls run under the invoke deadline. +type cryptoStub struct{ base } + +var _ crypto.Crypto = (*cryptoStub)(nil) + +func (s *cryptoStub) EncryptSecret(plaintext string) (string, error) { + resp := &abiv1.CryptoEncryptSecretResponse{} + if err := s.invoke(context.Background(), "encrypt_secret", &abiv1.CryptoEncryptSecretRequest{Plaintext: plaintext}, resp); err != nil { + return "", err + } + return resp.GetCiphertext(), nil +} + +func (s *cryptoStub) DecryptSecret(ciphertext string) (string, error) { + resp := &abiv1.CryptoDecryptSecretResponse{} + if err := s.invoke(context.Background(), "decrypt_secret", &abiv1.CryptoDecryptSecretRequest{Ciphertext: ciphertext}, resp); err != nil { + return "", err + } + return resp.GetPlaintext(), nil +} diff --git a/plugin/wasmguest/caps/datasources.go b/plugin/wasmguest/caps/datasources.go new file mode 100644 index 0000000..9306114 --- /dev/null +++ b/plugin/wasmguest/caps/datasources.go @@ -0,0 +1,46 @@ +package caps + +import ( + "context" + "encoding/json" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/datasources" + "github.com/google/uuid" +) + +// datasourcesStub implements datasources.Datasources over datasources.* +// capability calls. Items ([]any) and Meta (map[string]any) cross as JSON. +type datasourcesStub struct{ base } + +var _ datasources.Datasources = (*datasourcesStub)(nil) + +func (s *datasourcesStub) ResolveBucket(ctx context.Context, bucketID uuid.UUID) (*datasources.Result, error) { + resp := &abiv1.DatasourcesResolveBucketResponse{} + req := &abiv1.DatasourcesResolveBucketRequest{BucketId: bucketID.String()} + if err := s.invoke(ctx, "resolve_bucket", req, resp); err != nil { + return nil, err + } + return datasourceResultFromProto(resp.GetResult()), nil +} + +func (s *datasourcesStub) ResolveBucketByKey(ctx context.Context, bucketKey string) (*datasources.Result, error) { + resp := &abiv1.DatasourcesResolveBucketByKeyResponse{} + req := &abiv1.DatasourcesResolveBucketByKeyRequest{BucketKey: bucketKey} + if err := s.invoke(ctx, "resolve_bucket_by_key", req, resp); err != nil { + return nil, err + } + return datasourceResultFromProto(resp.GetResult()), nil +} + +func datasourceResultFromProto(r *abiv1.DatasourceResult) *datasources.Result { + if r == nil { + return nil + } + out := &datasources.Result{Total: int(r.GetTotal())} + if items := r.GetItemsJson(); len(items) > 0 { + _ = json.Unmarshal(items, &out.Items) + } + out.Meta = unmarshalMap(r.GetMetaJson()) + return out +} diff --git a/plugin/wasmguest/caps/email.go b/plugin/wasmguest/caps/email.go new file mode 100644 index 0000000..45a7561 --- /dev/null +++ b/plugin/wasmguest/caps/email.go @@ -0,0 +1,19 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" +) + +// emailStub implements plugin.EmailSender over the email.send capability call. +// The interface takes no context; the call runs under the invoke deadline. +type emailStub struct{ base } + +var _ plugin.EmailSender = (*emailStub)(nil) + +func (s *emailStub) Send(to, subject, body string) error { + req := &abiv1.EmailSendRequest{To: to, Subject: subject, Body: body} + return s.invoke(context.Background(), "send", req, &abiv1.EmailSendResponse{}) +} diff --git a/plugin/wasmguest/caps/embeddings.go b/plugin/wasmguest/caps/embeddings.go new file mode 100644 index 0000000..659e50e --- /dev/null +++ b/plugin/wasmguest/caps/embeddings.go @@ -0,0 +1,46 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "github.com/google/uuid" +) + +// embeddingsStub implements plugin.EmbeddingService over embeddings.* +// capability calls. +type embeddingsStub struct{ base } + +var _ plugin.EmbeddingService = (*embeddingsStub)(nil) + +func (s *embeddingsStub) GenerateEmbedding(ctx context.Context, text string) ([]float32, error) { + resp := &abiv1.EmbeddingsGenerateEmbeddingResponse{} + req := &abiv1.EmbeddingsGenerateEmbeddingRequest{Text: text} + if err := s.invoke(ctx, "generate_embedding", req, resp); err != nil { + return nil, err + } + return resp.GetEmbedding(), nil +} + +func (s *embeddingsStub) EmbedContent(ctx context.Context, sourceType string, sourceID uuid.UUID, text string) (bool, error) { + resp := &abiv1.EmbeddingsEmbedContentResponse{} + req := &abiv1.EmbeddingsEmbedContentRequest{ + SourceType: sourceType, + SourceId: sourceID.String(), + Text: text, + } + if err := s.invoke(ctx, "embed_content", req, resp); err != nil { + return false, err + } + return resp.GetEmbedded(), nil +} + +// IsAvailable has no error channel; a transport failure reports unavailable. +func (s *embeddingsStub) IsAvailable() bool { + resp := &abiv1.EmbeddingsIsAvailableResponse{} + if err := s.invoke(context.Background(), "is_available", &abiv1.EmbeddingsIsAvailableRequest{}, resp); err != nil { + return false + } + return resp.GetAvailable() +} diff --git a/plugin/wasmguest/caps/gating.go b/plugin/wasmguest/caps/gating.go new file mode 100644 index 0000000..523dbfe --- /dev/null +++ b/plugin/wasmguest/caps/gating.go @@ -0,0 +1,51 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/gating" + "github.com/google/uuid" +) + +// gatingStub implements gating.Gating over gating.* capability calls. +type gatingStub struct{ base } + +var _ gating.Gating = (*gatingStub)(nil) + +func (s *gatingStub) GetSubscriberTierLevel(ctx context.Context, userID uuid.UUID) (int, error) { + resp := &abiv1.GatingGetSubscriberTierLevelResponse{} + req := &abiv1.GatingGetSubscriberTierLevelRequest{UserId: userID.String()} + if err := s.invoke(ctx, "get_subscriber_tier_level", req, resp); err != nil { + return 0, err + } + return int(resp.GetLevel()), nil +} + +// EvaluateAccess mirrors the host's decision. The rule evaluation is a pure, +// deterministic function (gating.EvaluateAccess) so the interface exposes no +// error channel; the call still crosses the boundary to keep host and guest +// on identical logic, and falls back to the local pure function if the +// transport is unavailable. +func (s *gatingStub) EvaluateAccess(userTierLevel int, rule *gating.AccessRule) gating.AccessResult { + req := &abiv1.GatingEvaluateAccessRequest{UserTierLevel: int32(userTierLevel)} + if rule != nil { + req.Rule = &abiv1.AccessRule{ + MinTierLevel: int32(rule.MinTierLevel), + OverrideTierId: rule.OverrideTierID, + TeaserMode: rule.TeaserMode, + TeaserPercent: int32(rule.TeaserPercent), + } + } + resp := &abiv1.GatingEvaluateAccessResponse{} + if err := s.invoke(context.Background(), "evaluate_access", req, resp); err != nil { + return gating.EvaluateAccess(userTierLevel, rule) + } + r := resp.GetResult() + return gating.AccessResult{ + HasAccess: r.GetHasAccess(), + TeaserMode: r.GetTeaserMode(), + TeaserPercent: int(r.GetTeaserPercent()), + RequiredLevel: int(r.GetRequiredLevel()), + } +} diff --git a/plugin/wasmguest/caps/jobs.go b/plugin/wasmguest/caps/jobs.go new file mode 100644 index 0000000..d054ea9 --- /dev/null +++ b/plugin/wasmguest/caps/jobs.go @@ -0,0 +1,18 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" +) + +// jobsStub implements plugin.JobRunner over the jobs.submit capability call. +type jobsStub struct{ base } + +var _ plugin.JobRunner = (*jobsStub)(nil) + +func (s *jobsStub) Submit(ctx context.Context, jobType string, config []byte) error { + req := &abiv1.JobsSubmitRequest{JobType: jobType, ConfigJson: config} + return s.invoke(ctx, "submit", req, &abiv1.JobsSubmitResponse{}) +} diff --git a/plugin/wasmguest/caps/media.go b/plugin/wasmguest/caps/media.go new file mode 100644 index 0000000..1ea9efb --- /dev/null +++ b/plugin/wasmguest/caps/media.go @@ -0,0 +1,36 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "github.com/google/uuid" +) + +// mediaStub implements plugin.Media over the media.deposit capability call. +type mediaStub struct{ base } + +var _ plugin.Media = (*mediaStub)(nil) + +func (s *mediaStub) Deposit(ctx context.Context, deposit plugin.MediaDeposit) (plugin.MediaResult, error) { + req := &abiv1.MediaDepositRequest{ + Filename: deposit.Filename, + Data: deposit.Data, + AltText: deposit.AltText, + Folder: deposit.Folder, + Source: deposit.Source, + } + if deposit.ID != uuid.Nil { + req.Id = deposit.ID.String() + } + resp := &abiv1.MediaDepositResponse{} + if err := s.invoke(ctx, "deposit", req, resp); err != nil { + return plugin.MediaResult{}, err + } + return plugin.MediaResult{ + ID: parseUUID(resp.GetId()), + Ref: resp.GetRef(), + Created: resp.GetCreated(), + }, nil +} diff --git a/plugin/wasmguest/caps/menus.go b/plugin/wasmguest/caps/menus.go new file mode 100644 index 0000000..30c3d18 --- /dev/null +++ b/plugin/wasmguest/caps/menus.go @@ -0,0 +1,55 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/menus" + "github.com/google/uuid" +) + +// menusStub implements menus.Menus over menus.* capability calls. +type menusStub struct{ base } + +var _ menus.Menus = (*menusStub)(nil) + +func (s *menusStub) GetMenuByName(ctx context.Context, name string) (*menus.Menu, error) { + resp := &abiv1.MenusGetMenuByNameResponse{} + if err := s.invoke(ctx, "get_menu_by_name", &abiv1.MenusGetMenuByNameRequest{Name: name}, resp); err != nil { + return nil, err + } + m := resp.GetMenu() + if m == nil { + return nil, nil + } + return &menus.Menu{ID: parseUUID(m.GetId()), Name: m.GetName()}, nil +} + +func (s *menusStub) GetMenuItems(ctx context.Context, menuID uuid.UUID) ([]menus.MenuItem, error) { + resp := &abiv1.MenusGetMenuItemsResponse{} + req := &abiv1.MenusGetMenuItemsRequest{MenuId: menuID.String()} + if err := s.invoke(ctx, "get_menu_items", req, resp); err != nil { + return nil, err + } + items := make([]menus.MenuItem, 0, len(resp.GetItems())) + for _, it := range resp.GetItems() { + mi := menus.MenuItem{ + ID: parseUUID(it.GetId()), + MenuID: parseUUID(it.GetMenuId()), + Label: it.GetLabel(), + URL: it.GetUrl(), + PageSlug: it.GetPageSlug(), + SortOrder: it.GetSortOrder(), + OpenInNewTab: it.GetOpenInNewTab(), + CssClass: it.GetCssClass(), + ItemType: it.GetItemType(), + Icon: it.GetIcon(), + } + if it.ParentId != nil { + pid := parseUUID(it.GetParentId()) + mi.ParentID = &pid + } + items = append(items, mi) + } + return items, nil +} diff --git a/plugin/wasmguest/caps/provisioner.go b/plugin/wasmguest/caps/provisioner.go new file mode 100644 index 0000000..d583bb4 --- /dev/null +++ b/plugin/wasmguest/caps/provisioner.go @@ -0,0 +1,189 @@ +package caps + +import ( + "context" + "encoding/json" + "fmt" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "github.com/google/uuid" +) + +// provisionerStub implements plugin.Provisioner over provisioner.* capability +// calls (WO-WZ-019). This is a LOAD-TIME capability: call it from the Load +// hook. RegisterWithProvisioner still receives a no-op at Register/DESCRIBE +// time (host functions are stubbed to fail there), so seed logic must move to +// Load — see core/docs/wasm-abi.md. +// +// Most Provisioner methods carry no context (the Go interface predates the +// ABI); calls run under the ambient invoke deadline the host enforces. +type provisionerStub struct{ base } + +var _ plugin.Provisioner = (*provisionerStub)(nil) + +func (s *provisionerStub) EnsureDataTable(config plugin.DataTableConfig) error { + req := &abiv1.ProvisionerEnsureDataTableRequest{ + Key: config.Key, + Name: config.Name, + Description: config.Description, + SchemaJson: config.Schema, + PrimaryKey: config.PrimaryKey, + } + return s.invoke(context.Background(), "ensure_data_table", req, &abiv1.ProvisionerEnsureDataTableResponse{}) +} + +func (s *provisionerStub) MergeSiteSettings(defaults map[string]any) error { + defaultsJSON, err := json.Marshal(defaults) + if err != nil { + return fmt.Errorf("provisioner.merge_site_settings: marshal defaults: %w", err) + } + req := &abiv1.ProvisionerMergeSiteSettingsRequest{DefaultsJson: defaultsJSON} + return s.invoke(context.Background(), "merge_site_settings", req, &abiv1.ProvisionerMergeSiteSettingsResponse{}) +} + +func (s *provisionerStub) EnsureSetting(key string, defaultValue any) error { + valueJSON, err := json.Marshal(defaultValue) + if err != nil { + return fmt.Errorf("provisioner.ensure_setting: marshal value: %w", err) + } + req := &abiv1.ProvisionerEnsureSettingRequest{Key: key, DefaultValueJson: valueJSON} + return s.invoke(context.Background(), "ensure_setting", req, &abiv1.ProvisionerEnsureSettingResponse{}) +} + +func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error { + seed := &abiv1.PageSeed{ + Slug: config.Slug, + ParentSlug: config.ParentSlug, + Title: config.Title, + TemplateKey: config.TemplateKey, + DetailSourceType: config.DetailSourceType, + DetailSourceKey: config.DetailSourceKey, + DetailSlugField: config.DetailSlugField, + ReconcileBlocks: config.ReconcileBlocks, + ReconcileTemplate: config.ReconcileTemplate, + } + for _, b := range config.Blocks { + contentJSON, err := json.Marshal(b.Content) + if err != nil { + return fmt.Errorf("provisioner.ensure_page: block %q: marshal content: %w", b.BlockKey, err) + } + seed.Blocks = append(seed.Blocks, &abiv1.PageBlock{ + BlockKey: b.BlockKey, + Title: b.Title, + ContentJson: contentJSON, + HtmlContent: b.HtmlContent, + Slot: b.Slot, + SortOrder: b.SortOrder, + }) + } + req := &abiv1.ProvisionerEnsurePageRequest{Page: seed} + return s.invoke(context.Background(), "ensure_page", req, &abiv1.ProvisionerEnsurePageResponse{}) +} + +func (s *provisionerStub) OverrideSiteSettings(overrides map[string]any) error { + overridesJSON, err := json.Marshal(overrides) + if err != nil { + return fmt.Errorf("provisioner.override_site_settings: marshal overrides: %w", err) + } + req := &abiv1.ProvisionerOverrideSiteSettingsRequest{OverridesJson: overridesJSON} + return s.invoke(context.Background(), "override_site_settings", req, &abiv1.ProvisionerOverrideSiteSettingsResponse{}) +} + +func (s *provisionerStub) EnsureMenuItem(menuName string, config plugin.MenuItemConfig) error { + req := &abiv1.ProvisionerEnsureMenuItemRequest{ + MenuName: menuName, + Label: config.Label, + Url: config.URL, + PageSlug: config.PageSlug, + SortOrder: config.SortOrder, + } + return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{}) +} + +func (s *provisionerStub) RegisterEmbeddingConfig(config plugin.EmbeddingConfigDef) error { + req := &abiv1.ProvisionerRegisterEmbeddingConfigRequest{ + TableKey: config.TableKey, + TextTemplate: config.TextTemplate, + Enabled: config.Enabled, + } + return s.invoke(context.Background(), "register_embedding_config", req, &abiv1.ProvisionerRegisterEmbeddingConfigResponse{}) +} + +// EnsureEmbed crosses the template-rendered surface only: EmbedConfig. +// RenderFunc is a function value that cannot serialize, so an ABI-provisioned +// embed must carry a Template (a RenderFunc-only embed returns an error +// rather than silently dropping the renderer). +func (s *provisionerStub) EnsureEmbed(config plugin.EmbedConfig) error { + if config.RenderFunc != nil && config.Template == "" { + return fmt.Errorf("provisioner.ensure_embed: embed %q uses RenderFunc, which cannot cross the wasm ABI — provide a Template instead", config.Key) + } + req := &abiv1.ProvisionerEnsureEmbedRequest{ + Key: config.Key, + Title: config.Title, + Description: config.Description, + Icon: config.Icon, + LabelField: config.LabelField, + Template: config.Template, + DataSourceType: config.DataSource.Type, + DataSourceTableKey: config.DataSource.TableKey, + } + return s.invoke(context.Background(), "ensure_embed", req, &abiv1.ProvisionerEnsureEmbedResponse{}) +} + +func (s *provisionerStub) EnsureJobSchedule(config plugin.JobScheduleConfig) error { + req := &abiv1.ProvisionerEnsureJobScheduleRequest{ + JobType: config.JobType, + CronExpression: config.CronExpression, + ConfigJson: config.Config, + } + return s.invoke(context.Background(), "ensure_job_schedule", req, &abiv1.ProvisionerEnsureJobScheduleResponse{}) +} + +func (s *provisionerStub) UpdateDataTableRowField(ctx context.Context, rowID uuid.UUID, fieldKey string, value any) error { + valueJSON, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("provisioner.update_data_table_row_field: marshal value: %w", err) + } + req := &abiv1.ProvisionerUpdateDataTableRowFieldRequest{ + RowId: rowID.String(), + FieldKey: fieldKey, + ValueJson: valueJSON, + } + return s.invoke(ctx, "update_data_table_row_field", req, &abiv1.ProvisionerUpdateDataTableRowFieldResponse{}) +} + +func (s *provisionerStub) DisableOrphanedJobSchedules(registeredTypes []string) error { + req := &abiv1.ProvisionerDisableOrphanedJobSchedulesRequest{RegisteredTypes: registeredTypes} + return s.invoke(context.Background(), "disable_orphaned_job_schedules", req, &abiv1.ProvisionerDisableOrphanedJobSchedulesResponse{}) +} + +func (s *provisionerStub) EnsurePlugin(name string) error { + req := &abiv1.ProvisionerEnsurePluginRequest{Name: name} + return s.invoke(context.Background(), "ensure_plugin", req, &abiv1.ProvisionerEnsurePluginResponse{}) +} + +func (s *provisionerStub) EnsureCustomColor(config plugin.CustomColorConfig) error { + req := &abiv1.ProvisionerEnsureCustomColorRequest{ + Name: config.Name, + LightValue: config.LightValue, + DarkValue: config.DarkValue, + Source: config.Source, + } + return s.invoke(context.Background(), "ensure_custom_color", req, &abiv1.ProvisionerEnsureCustomColorResponse{}) +} + +func (s *provisionerStub) EnsureMedia(deposit plugin.MediaDeposit) error { + if deposit.ID == uuid.Nil { + return fmt.Errorf("provisioner.ensure_media: MediaDeposit.ID is required (the deterministic, template-referable key)") + } + req := &abiv1.ProvisionerEnsureMediaRequest{ + Id: deposit.ID.String(), + Filename: deposit.Filename, + Data: deposit.Data, + AltText: deposit.AltText, + Folder: deposit.Folder, + Source: deposit.Source, + } + return s.invoke(context.Background(), "ensure_media", req, &abiv1.ProvisionerEnsureMediaResponse{}) +} diff --git a/plugin/wasmguest/caps/rag.go b/plugin/wasmguest/caps/rag.go new file mode 100644 index 0000000..a2b87a5 --- /dev/null +++ b/plugin/wasmguest/caps/rag.go @@ -0,0 +1,78 @@ +package caps + +import ( + "context" + "sort" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "github.com/google/uuid" +) + +// RAGStub implements plugin.RAGService. Query and OnContentChanged marshal out +// to rag.* capability calls, while RegisterContentFetcher records the fetcher +// guest-side: the host re-indexes by inverting the call as a HOOK_RAG_FETCH +// callback, so the wasm shim reads the recorded fetchers to dispatch it. The +// stub is exported so the shim can reach FetcherTypes/Fetcher. +type RAGStub struct { + base + fetchers map[string]plugin.ContentFetcher +} + +var _ plugin.RAGService = (*RAGStub)(nil) + +// NewRAGStub builds a RAG stub with the given transport. A nil transport +// (DESCRIBE probe, native build) still records fetchers so their content +// types can be captured into the manifest. +func NewRAGStub(call CallFunc) *RAGStub { + return &RAGStub{ + base: base{family: "rag", call: call}, + fetchers: make(map[string]plugin.ContentFetcher), + } +} + +func (s *RAGStub) RegisterContentFetcher(contentType string, fetcher plugin.ContentFetcher) { + s.fetchers[contentType] = fetcher +} + +func (s *RAGStub) Query(ctx context.Context, query string, limit int) ([]plugin.RAGResult, error) { + req := &abiv1.RagQueryRequest{Query: query, Limit: int32(limit)} + resp := &abiv1.RagQueryResponse{} + if err := s.invoke(ctx, "query", req, resp); err != nil { + return nil, err + } + results := make([]plugin.RAGResult, 0, len(resp.GetResults())) + for _, r := range resp.GetResults() { + results = append(results, plugin.RAGResult{ + Content: r.GetContent(), + Score: r.GetScore(), + Metadata: r.GetMetadata(), + }) + } + return results, nil +} + +// OnContentChanged has no error channel; a transport failure is dropped (the +// host will re-index on its own schedule regardless). +func (s *RAGStub) OnContentChanged(ctx context.Context, contentType string, contentID uuid.UUID) { + req := &abiv1.RagOnContentChangedRequest{ContentType: contentType, ContentId: contentID.String()} + _ = s.invoke(ctx, "on_content_changed", req, &abiv1.RagOnContentChangedResponse{}) +} + +// Fetcher returns the content fetcher registered for a content type, for +// HOOK_RAG_FETCH dispatch by the wasm shim. +func (s *RAGStub) Fetcher(contentType string) (plugin.ContentFetcher, bool) { + f, ok := s.fetchers[contentType] + return f, ok +} + +// FetcherTypes lists the registered content-fetcher types in sorted order +// (deterministic manifest capture). +func (s *RAGStub) FetcherTypes() []string { + types := make([]string, 0, len(s.fetchers)) + for k := range s.fetchers { + types = append(types, k) + } + sort.Strings(types) + return types +} diff --git a/plugin/wasmguest/caps/reviews.go b/plugin/wasmguest/caps/reviews.go new file mode 100644 index 0000000..41bdacf --- /dev/null +++ b/plugin/wasmguest/caps/reviews.go @@ -0,0 +1,35 @@ +package caps + +import ( + "context" + "encoding/json" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" +) + +// reviewsStub implements plugin.ReviewSubmitter over the reviews.submit_review +// capability call. +type reviewsStub struct{ base } + +var _ plugin.ReviewSubmitter = (*reviewsStub)(nil) + +func (s *reviewsStub) SubmitReview(ctx context.Context, params plugin.SubmitReviewParams) (string, error) { + req := &abiv1.ReviewsSubmitReviewRequest{ + TableId: params.TableID.String(), + RowId: params.RowID.String(), + OverallRating: params.OverallRating, + ReviewText: params.ReviewText, + Photos: params.Photos, + } + if params.Ratings != nil { + if raw, err := json.Marshal(params.Ratings); err == nil { + req.RatingsJson = raw + } + } + resp := &abiv1.ReviewsSubmitReviewResponse{} + if err := s.invoke(ctx, "submit_review", req, resp); err != nil { + return "", err + } + return resp.GetReviewId(), nil +} diff --git a/plugin/wasmguest/caps/settings.go b/plugin/wasmguest/caps/settings.go new file mode 100644 index 0000000..daddb9e --- /dev/null +++ b/plugin/wasmguest/caps/settings.go @@ -0,0 +1,53 @@ +package caps + +import ( + "context" + "encoding/json" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/settings" +) + +// settingsStub implements both settings.Settings and settings.Updater over +// settings.* capability calls. One instance backs both CoreServices fields. +type settingsStub struct{ base } + +var ( + _ settings.Settings = (*settingsStub)(nil) + _ settings.Updater = (*settingsStub)(nil) +) + +func (s *settingsStub) GetSiteSettings(ctx context.Context) (map[string]any, error) { + resp := &abiv1.SettingsGetSiteSettingsResponse{} + if err := s.invoke(ctx, "get_site_settings", &abiv1.SettingsGetSiteSettingsRequest{}, resp); err != nil { + return nil, err + } + return unmarshalMap(resp.GetSettingsJson()), nil +} + +func (s *settingsStub) GetPluginSettings(ctx context.Context, pluginName string) (map[string]any, error) { + resp := &abiv1.SettingsGetPluginSettingsResponse{} + req := &abiv1.SettingsGetPluginSettingsRequest{PluginName: pluginName} + if err := s.invoke(ctx, "get_plugin_settings", req, resp); err != nil { + return nil, err + } + return unmarshalMap(resp.GetSettingsJson()), nil +} + +func (s *settingsStub) UpdateSiteSetting(ctx context.Context, key string, value any) error { + valueJSON, err := json.Marshal(value) + if err != nil { + return err + } + req := &abiv1.SettingsUpdateSiteSettingRequest{Key: key, ValueJson: valueJSON} + return s.invoke(ctx, "update_site_setting", req, &abiv1.SettingsUpdateSiteSettingResponse{}) +} + +func (s *settingsStub) UpdatePluginSettings(ctx context.Context, pluginName string, settingsMap map[string]any) error { + settingsJSON, err := json.Marshal(settingsMap) + if err != nil { + return err + } + req := &abiv1.SettingsUpdatePluginSettingsRequest{PluginName: pluginName, SettingsJson: settingsJSON} + return s.invoke(ctx, "update_plugin_settings", req, &abiv1.SettingsUpdatePluginSettingsResponse{}) +} diff --git a/plugin/wasmguest/caps/subscriptions.go b/plugin/wasmguest/caps/subscriptions.go new file mode 100644 index 0000000..579009b --- /dev/null +++ b/plugin/wasmguest/caps/subscriptions.go @@ -0,0 +1,91 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/subscriptions" + "github.com/google/uuid" +) + +// subscriptionsStub implements subscriptions.Subscriptions over +// subscriptions.* capability calls. +type subscriptionsStub struct{ base } + +var _ subscriptions.Subscriptions = (*subscriptionsStub)(nil) + +func (s *subscriptionsStub) GetUserTierLevel(ctx context.Context, userID uuid.UUID) (*subscriptions.TierLevel, error) { + resp := &abiv1.SubscriptionsGetUserTierLevelResponse{} + req := &abiv1.SubscriptionsGetUserTierLevelRequest{UserId: userID.String()} + if err := s.invoke(ctx, "get_user_tier_level", req, resp); err != nil { + return nil, err + } + tl := resp.GetTierLevel() + if tl == nil { + return nil, nil + } + return &subscriptions.TierLevel{Level: int(tl.GetLevel()), Features: tl.GetFeatures()}, nil +} + +func (s *subscriptionsStub) GetTierBySlug(ctx context.Context, slug string) (*subscriptions.Tier, error) { + resp := &abiv1.SubscriptionsGetTierBySlugResponse{} + req := &abiv1.SubscriptionsGetTierBySlugRequest{Slug: slug} + if err := s.invoke(ctx, "get_tier_by_slug", req, resp); err != nil { + return nil, err + } + t := resp.GetTier() + if t == nil { + return nil, nil + } + tier := tierFromProto(t) + return &tier, nil +} + +func (s *subscriptionsStub) ListTiers(ctx context.Context) ([]subscriptions.Tier, error) { + resp := &abiv1.SubscriptionsListTiersResponse{} + if err := s.invoke(ctx, "list_tiers", &abiv1.SubscriptionsListTiersRequest{}, resp); err != nil { + return nil, err + } + tiers := make([]subscriptions.Tier, 0, len(resp.GetTiers())) + for _, t := range resp.GetTiers() { + tiers = append(tiers, tierFromProto(t)) + } + return tiers, nil +} + +func (s *subscriptionsStub) ListActivePlans(ctx context.Context, tierID uuid.UUID) ([]subscriptions.Plan, error) { + resp := &abiv1.SubscriptionsListActivePlansResponse{} + req := &abiv1.SubscriptionsListActivePlansRequest{TierId: tierID.String()} + if err := s.invoke(ctx, "list_active_plans", req, resp); err != nil { + return nil, err + } + plans := make([]subscriptions.Plan, 0, len(resp.GetPlans())) + for _, p := range resp.GetPlans() { + plan := subscriptions.Plan{ + ID: parseUUID(p.GetId()), + TierID: parseUUID(p.GetTierId()), + BillingInterval: p.GetBillingInterval(), + Amount: p.GetAmount(), + Currency: p.GetCurrency(), + IsActive: p.GetIsActive(), + } + if ts := p.GetCreatedAt(); ts != nil { + plan.CreatedAt = ts.AsTime() + } + plans = append(plans, plan) + } + return plans, nil +} + +func tierFromProto(t *abiv1.Tier) subscriptions.Tier { + return subscriptions.Tier{ + ID: parseUUID(t.GetId()), + Name: t.GetName(), + Slug: t.GetSlug(), + Level: int(t.GetLevel()), + Description: t.GetDescription(), + Features: t.GetFeatures(), + IsDefault: t.GetIsDefault(), + Position: int(t.GetPosition()), + } +} diff --git a/plugin/wasmguest/caps/testdata/golden/ai_text_call_req.pb b/plugin/wasmguest/caps/testdata/golden/ai_text_call_req.pb new file mode 100644 index 0000000..f3cf862 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/ai_text_call_req.pb @@ -0,0 +1,2 @@ + + summarizesysuser \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/ai_text_call_resp.pb b/plugin/wasmguest/caps/testdata/golden/ai_text_call_resp.pb new file mode 100644 index 0000000..510eec0 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/ai_text_call_resp.pb @@ -0,0 +1,2 @@ + + generated \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/ai_tools_register_req.pb b/plugin/wasmguest/caps/testdata/golden/ai_tools_register_req.pb new file mode 100644 index 0000000..b7c80fa --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/ai_tools_register_req.pb @@ -0,0 +1,2 @@ + +lookupLookupd"{"type":"object"} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/ai_tools_register_resp.pb b/plugin/wasmguest/caps/testdata/golden/ai_tools_register_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/badges_refresh_badges_req.pb b/plugin/wasmguest/caps/testdata/golden/badges_refresh_badges_req.pb new file mode 100644 index 0000000..a05bc9d --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/badges_refresh_badges_req.pb @@ -0,0 +1,2 @@ + +$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/badges_refresh_badges_resp.pb b/plugin/wasmguest/caps/testdata/golden/badges_refresh_badges_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/bridge_get_service_req.pb b/plugin/wasmguest/caps/testdata/golden/bridge_get_service_req.pb new file mode 100644 index 0000000..33f1edb --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/bridge_get_service_req.pb @@ -0,0 +1,2 @@ + + symposiumsearch \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/bridge_get_service_resp.pb b/plugin/wasmguest/caps/testdata/golden/bridge_get_service_resp.pb new file mode 100644 index 0000000..e19a122 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/bridge_get_service_resp.pb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/bridge_invoke_req.pb b/plugin/wasmguest/caps/testdata/golden/bridge_invoke_req.pb new file mode 100644 index 0000000..8ee401d --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/bridge_invoke_req.pb @@ -0,0 +1,3 @@ + + symposiumseeder +seed_forum"{"n":1} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/bridge_invoke_resp.pb b/plugin/wasmguest/caps/testdata/golden/bridge_invoke_resp.pb new file mode 100644 index 0000000..c78e5e1 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/bridge_invoke_resp.pb @@ -0,0 +1,2 @@ + + {"ok":true} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/bridge_register_service_req.pb b/plugin/wasmguest/caps/testdata/golden/bridge_register_service_req.pb new file mode 100644 index 0000000..33f1edb --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/bridge_register_service_req.pb @@ -0,0 +1,2 @@ + + symposiumsearch \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/bridge_register_service_resp.pb b/plugin/wasmguest/caps/testdata/golden/bridge_register_service_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/content_block_note_to_html_req.pb b/plugin/wasmguest/caps/testdata/golden/content_block_note_to_html_req.pb new file mode 100644 index 0000000..3d1a6d5 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_block_note_to_html_req.pb @@ -0,0 +1,2 @@ + +{"type":"doc"} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_block_note_to_html_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_block_note_to_html_resp.pb new file mode 100644 index 0000000..2a94743 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_block_note_to_html_resp.pb @@ -0,0 +1,2 @@ + +

hi

\ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_create_page_req.pb b/plugin/wasmguest/caps/testdata/golden/content_create_page_req.pb new file mode 100644 index 0000000..ecef402 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_create_page_req.pb @@ -0,0 +1,2 @@ + +/pricingPricing"landing \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_create_page_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_create_page_resp.pb new file mode 100644 index 0000000..0e950ea --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_create_page_resp.pb @@ -0,0 +1,2 @@ + +$44444444-4444-4444-4444-444444444444 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_generate_excerpt_req.pb b/plugin/wasmguest/caps/testdata/golden/content_generate_excerpt_req.pb new file mode 100644 index 0000000..6cdbc2a --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_generate_excerpt_req.pb @@ -0,0 +1,2 @@ + +

long text

 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_generate_excerpt_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_generate_excerpt_resp.pb new file mode 100644 index 0000000..ff132f6 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_generate_excerpt_resp.pb @@ -0,0 +1,2 @@ + +short \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_get_author_profile_req.pb b/plugin/wasmguest/caps/testdata/golden/content_get_author_profile_req.pb new file mode 100644 index 0000000..b14e671 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_get_author_profile_req.pb @@ -0,0 +1,2 @@ + +$22222222-2222-2222-2222-222222222222 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_get_author_profile_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_get_author_profile_resp.pb new file mode 100644 index 0000000..fd8dffb --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_get_author_profile_resp.pb @@ -0,0 +1,5 @@ + +l +$22222222-2222-2222-2222-222222222222Adaada"hi*https://x/a.png2https://ada.dev: +ghada: +x@ada \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_get_page_req.pb b/plugin/wasmguest/caps/testdata/golden/content_get_page_req.pb new file mode 100644 index 0000000..fc830ef --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_get_page_req.pb @@ -0,0 +1,2 @@ + +about \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_get_page_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_get_page_resp.pb new file mode 100644 index 0000000..88727b1 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_get_page_resp.pb @@ -0,0 +1,3 @@ + +7 +$22222222-2222-2222-2222-222222222222aboutAbout Us \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_get_post_req.pb b/plugin/wasmguest/caps/testdata/golden/content_get_post_req.pb new file mode 100644 index 0000000..19b5c9b --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_get_post_req.pb @@ -0,0 +1,2 @@ + +hello \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_get_post_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_get_post_resp.pb new file mode 100644 index 0000000..877d679 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_get_post_resp.pb @@ -0,0 +1,3 @@ + +g +$22222222-2222-2222-2222-222222222222helloHello"hi*media:x2$22222222-2222-2222-2222-222222222222 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_list_posts_req.pb b/plugin/wasmguest/caps/testdata/golden/content_list_posts_req.pb new file mode 100644 index 0000000..bb5ca68 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_list_posts_req.pb @@ -0,0 +1,2 @@ + +0 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_list_posts_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_list_posts_resp.pb new file mode 100644 index 0000000..6cf61b5 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_list_posts_resp.pb @@ -0,0 +1,5 @@ + +z +$22222222-2222-2222-2222-222222222222firstFirst"one*media:12$22222222-2222-2222-2222-222222222222BAdaJadaRȞ +s +$88888888-8888-8888-8888-888888888888secondSecond"two2$22222222-2222-2222-2222-222222222222BAdaJadaRȞ \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_publish_page_req.pb b/plugin/wasmguest/caps/testdata/golden/content_publish_page_req.pb new file mode 100644 index 0000000..f4e8311 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_publish_page_req.pb @@ -0,0 +1,2 @@ + +$44444444-4444-4444-4444-444444444444 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_publish_page_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_publish_page_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_req.pb b/plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_req.pb new file mode 100644 index 0000000..b70c2a2 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_req.pb @@ -0,0 +1,4 @@ + +$44444444-4444-4444-4444-4444444444445 +htmlHero{"align":"center"}"

Hi

*main0 +ctaCTA{"label":"Go"}0 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_set_page_blocks_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/content_set_page_seo_req.pb b/plugin/wasmguest/caps/testdata/golden/content_set_page_seo_req.pb new file mode 100644 index 0000000..d3fabb4 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_set_page_seo_req.pb @@ -0,0 +1,2 @@ + +$44444444-4444-4444-4444-444444444444Pricing — AcmePlans and pricing. \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_set_page_seo_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_set_page_seo_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/content_slugify_req.pb b/plugin/wasmguest/caps/testdata/golden/content_slugify_req.pb new file mode 100644 index 0000000..0cbd2cd --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_slugify_req.pb @@ -0,0 +1,2 @@ + + Hello World \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_slugify_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_slugify_resp.pb new file mode 100644 index 0000000..67c72ed --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_slugify_resp.pb @@ -0,0 +1,2 @@ + + hello-world \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_strip_html_req.pb b/plugin/wasmguest/caps/testdata/golden/content_strip_html_req.pb new file mode 100644 index 0000000..8674672 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_strip_html_req.pb @@ -0,0 +1,2 @@ + + plain \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_strip_html_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_strip_html_resp.pb new file mode 100644 index 0000000..be7ef69 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_strip_html_resp.pb @@ -0,0 +1,2 @@ + +plain \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_upsert_post_req.pb b/plugin/wasmguest/caps/testdata/golden/content_upsert_post_req.pb new file mode 100644 index 0000000..00f65e3 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_upsert_post_req.pb @@ -0,0 +1,2 @@ + +helloHello{"type":"doc"}"hello*$22222222-2222-2222-2222-2222222222222$99999999-9999-9999-9999-9999999999998 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/content_upsert_post_resp.pb b/plugin/wasmguest/caps/testdata/golden/content_upsert_post_resp.pb new file mode 100644 index 0000000..8a8e203 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/content_upsert_post_resp.pb @@ -0,0 +1,2 @@ + +$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/crypto_decrypt_secret_req.pb b/plugin/wasmguest/caps/testdata/golden/crypto_decrypt_secret_req.pb new file mode 100644 index 0000000..b01188b --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/crypto_decrypt_secret_req.pb @@ -0,0 +1,2 @@ + +enc:abc \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/crypto_decrypt_secret_resp.pb b/plugin/wasmguest/caps/testdata/golden/crypto_decrypt_secret_resp.pb new file mode 100644 index 0000000..be7ef69 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/crypto_decrypt_secret_resp.pb @@ -0,0 +1,2 @@ + +plain \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/crypto_encrypt_secret_req.pb b/plugin/wasmguest/caps/testdata/golden/crypto_encrypt_secret_req.pb new file mode 100644 index 0000000..be7ef69 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/crypto_encrypt_secret_req.pb @@ -0,0 +1,2 @@ + +plain \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/crypto_encrypt_secret_resp.pb b/plugin/wasmguest/caps/testdata/golden/crypto_encrypt_secret_resp.pb new file mode 100644 index 0000000..b01188b --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/crypto_encrypt_secret_resp.pb @@ -0,0 +1,2 @@ + +enc:abc \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_by_key_req.pb b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_by_key_req.pb new file mode 100644 index 0000000..7cb24d0 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_by_key_req.pb @@ -0,0 +1,2 @@ + +featured \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_by_key_resp.pb b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_by_key_resp.pb new file mode 100644 index 0000000..0d791ca --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_by_key_resp.pb @@ -0,0 +1,3 @@ + + +[] \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_req.pb b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_req.pb new file mode 100644 index 0000000..a0eadfa --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_req.pb @@ -0,0 +1,2 @@ + +$66666666-6666-6666-6666-666666666666 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_resp.pb b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_resp.pb new file mode 100644 index 0000000..577cfab --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/datasources_resolve_bucket_resp.pb @@ -0,0 +1,4 @@ + +# +[{"id":1},{"id":2}] +{"page":1} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/email_send_req.pb b/plugin/wasmguest/caps/testdata/golden/email_send_req.pb new file mode 100644 index 0000000..91b50ff --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/email_send_req.pb @@ -0,0 +1,2 @@ + +to@x.comSubjBody \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/email_send_resp.pb b/plugin/wasmguest/caps/testdata/golden/email_send_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/embeddings_embed_content_req.pb b/plugin/wasmguest/caps/testdata/golden/embeddings_embed_content_req.pb new file mode 100644 index 0000000..e36942b --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/embeddings_embed_content_req.pb @@ -0,0 +1,2 @@ + +post$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbbtext \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/embeddings_embed_content_resp.pb b/plugin/wasmguest/caps/testdata/golden/embeddings_embed_content_resp.pb new file mode 100644 index 0000000..e19a122 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/embeddings_embed_content_resp.pb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/embeddings_generate_embedding_req.pb b/plugin/wasmguest/caps/testdata/golden/embeddings_generate_embedding_req.pb new file mode 100644 index 0000000..b2b71e4 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/embeddings_generate_embedding_req.pb @@ -0,0 +1,2 @@ + +text \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/embeddings_generate_embedding_resp.pb b/plugin/wasmguest/caps/testdata/golden/embeddings_generate_embedding_resp.pb new file mode 100644 index 0000000..5d83ec7 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/embeddings_generate_embedding_resp.pb @@ -0,0 +1,2 @@ + + =L>> \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/embeddings_is_available_req.pb b/plugin/wasmguest/caps/testdata/golden/embeddings_is_available_req.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/embeddings_is_available_resp.pb b/plugin/wasmguest/caps/testdata/golden/embeddings_is_available_resp.pb new file mode 100644 index 0000000..e19a122 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/embeddings_is_available_resp.pb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/gating_evaluate_access_req.pb b/plugin/wasmguest/caps/testdata/golden/gating_evaluate_access_req.pb new file mode 100644 index 0000000..aa0ac66 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/gating_evaluate_access_req.pb @@ -0,0 +1,2 @@ + +soft  \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/gating_evaluate_access_resp.pb b/plugin/wasmguest/caps/testdata/golden/gating_evaluate_access_resp.pb new file mode 100644 index 0000000..6f892fd --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/gating_evaluate_access_resp.pb @@ -0,0 +1,3 @@ + + +soft  \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/gating_get_subscriber_tier_level_req.pb b/plugin/wasmguest/caps/testdata/golden/gating_get_subscriber_tier_level_req.pb new file mode 100644 index 0000000..1bdd816 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/gating_get_subscriber_tier_level_req.pb @@ -0,0 +1,2 @@ + +$11111111-1111-1111-1111-111111111111 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/gating_get_subscriber_tier_level_resp.pb b/plugin/wasmguest/caps/testdata/golden/gating_get_subscriber_tier_level_resp.pb new file mode 100644 index 0000000..d65dd8f --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/gating_get_subscriber_tier_level_resp.pb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/jobs_progress_req.pb b/plugin/wasmguest/caps/testdata/golden/jobs_progress_req.pb new file mode 100644 index 0000000..555d89c --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/jobs_progress_req.pb @@ -0,0 +1 @@ +halfway \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/jobs_progress_resp.pb b/plugin/wasmguest/caps/testdata/golden/jobs_progress_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/jobs_submit_req.pb b/plugin/wasmguest/caps/testdata/golden/jobs_submit_req.pb new file mode 100644 index 0000000..ab25be6 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/jobs_submit_req.pb @@ -0,0 +1,2 @@ + +reindex {"full":true} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/jobs_submit_resp.pb b/plugin/wasmguest/caps/testdata/golden/jobs_submit_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/media_deposit_req.pb b/plugin/wasmguest/caps/testdata/golden/media_deposit_req.pb new file mode 100644 index 0000000..ee44428 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/media_deposit_req.pb @@ -0,0 +1,2 @@ + +$99999999-9999-9999-9999-999999999999hero.jpgbytes"alt*f2plugin \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/media_deposit_resp.pb b/plugin/wasmguest/caps/testdata/golden/media_deposit_resp.pb new file mode 100644 index 0000000..a6e3d9c --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/media_deposit_resp.pb @@ -0,0 +1,2 @@ + +$99999999-9999-9999-9999-999999999999*media:99999999-9999-9999-9999-999999999999 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/menus_get_menu_by_name_req.pb b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_by_name_req.pb new file mode 100644 index 0000000..68d8818 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_by_name_req.pb @@ -0,0 +1,2 @@ + +main \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/menus_get_menu_by_name_resp.pb b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_by_name_resp.pb new file mode 100644 index 0000000..1510231 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_by_name_resp.pb @@ -0,0 +1,3 @@ + +, +$33333333-3333-3333-3333-333333333333main \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/menus_get_menu_items_req.pb b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_items_req.pb new file mode 100644 index 0000000..3bde6cb --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_items_req.pb @@ -0,0 +1,2 @@ + +$33333333-3333-3333-3333-333333333333 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/menus_get_menu_items_resp.pb b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_items_resp.pb new file mode 100644 index 0000000..010df2b --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/menus_get_menu_items_resp.pb @@ -0,0 +1,3 @@ + + +$44444444-4444-4444-4444-444444444444$33333333-3333-3333-3333-333333333333Home"/*home2$55555555-5555-5555-5555-5555555555558@JnavRlinkZhome \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_disable_orphaned_job_schedules_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_disable_orphaned_job_schedules_req.pb new file mode 100644 index 0000000..0eba592 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_disable_orphaned_job_schedules_req.pb @@ -0,0 +1,3 @@ + +bounty_rotation +reindex \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_disable_orphaned_job_schedules_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_disable_orphaned_job_schedules_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_custom_color_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_custom_color_req.pb new file mode 100644 index 0000000..ed99cc1 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_custom_color_req.pb @@ -0,0 +1,2 @@ + +brandoklch(0.7 0.1 200)oklch(0.4 0.1 200)"myplugin \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_custom_color_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_custom_color_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_data_table_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_data_table_req.pb new file mode 100644 index 0000000..cdf29e9 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_data_table_req.pb @@ -0,0 +1,2 @@ + + playgrounds Playgroundsd" {"fields":[]}*id \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_data_table_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_data_table_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_embed_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_embed_req.pb new file mode 100644 index 0000000..a4d0fc2 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_embed_req.pb @@ -0,0 +1,2 @@ + +playground-cardPlayground Cardd"📍*name2
{{name}}
:rowB playgrounds \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_embed_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_embed_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_job_schedule_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_job_schedule_req.pb new file mode 100644 index 0000000..8e76632 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_job_schedule_req.pb @@ -0,0 +1,2 @@ + +bounty_rotation 0 3 * * *{"n":3} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_job_schedule_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_job_schedule_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_req.pb new file mode 100644 index 0000000..c233c34 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_req.pb @@ -0,0 +1,2 @@ + +$99999999-9999-9999-9999-999999999999hero.jpg"hero*seed2myplugin \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_media_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_req.pb new file mode 100644 index 0000000..c486d4f --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_req.pb @@ -0,0 +1,2 @@ + +mainDocs"/docs( \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_menu_item_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_req.pb new file mode 100644 index 0000000..1f5bb6f --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_req.pb @@ -0,0 +1,4 @@ + +4 +/Home"homepage* +htmlHero{"x":1}*main0H \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_page_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_plugin_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_plugin_req.pb new file mode 100644 index 0000000..d592105 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_plugin_req.pb @@ -0,0 +1,2 @@ + + symposium \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_plugin_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_plugin_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_setting_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_setting_req.pb new file mode 100644 index 0000000..21449b9 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_setting_req.pb @@ -0,0 +1,2 @@ + +review_dimensions {"max":5} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_setting_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_ensure_setting_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_req.pb new file mode 100644 index 0000000..770376a --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_req.pb @@ -0,0 +1,2 @@ + +{"site_name":"Acme"} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_merge_site_settings_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_override_site_settings_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_override_site_settings_req.pb new file mode 100644 index 0000000..2cd8036 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_override_site_settings_req.pb @@ -0,0 +1,2 @@ + +{"default_template":"homepage"} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_override_site_settings_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_override_site_settings_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_register_embedding_config_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_register_embedding_config_req.pb new file mode 100644 index 0000000..fd29f12 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_register_embedding_config_req.pb @@ -0,0 +1,2 @@ + + playgrounds{{name}} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_register_embedding_config_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_register_embedding_config_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_update_data_table_row_field_req.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_update_data_table_row_field_req.pb new file mode 100644 index 0000000..b032156 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/provisioner_update_data_table_row_field_req.pb @@ -0,0 +1,2 @@ + +$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbbstatus"active" \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/provisioner_update_data_table_row_field_resp.pb b/plugin/wasmguest/caps/testdata/golden/provisioner_update_data_table_row_field_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/rag_on_content_changed_req.pb b/plugin/wasmguest/caps/testdata/golden/rag_on_content_changed_req.pb new file mode 100644 index 0000000..6f286c1 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/rag_on_content_changed_req.pb @@ -0,0 +1,2 @@ + +post$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/rag_on_content_changed_resp.pb b/plugin/wasmguest/caps/testdata/golden/rag_on_content_changed_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/rag_query_req.pb b/plugin/wasmguest/caps/testdata/golden/rag_query_req.pb new file mode 100644 index 0000000..fc21e24 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/rag_query_req.pb @@ -0,0 +1,2 @@ + +q \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/rag_query_resp.pb b/plugin/wasmguest/caps/testdata/golden/rag_query_resp.pb new file mode 100644 index 0000000..fb42626 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/rag_query_resp.pb @@ -0,0 +1,4 @@ + + +chunk? +srcpost \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/reviews_submit_review_req.pb b/plugin/wasmguest/caps/testdata/golden/reviews_submit_review_req.pb new file mode 100644 index 0000000..6652b06 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/reviews_submit_review_req.pb @@ -0,0 +1,3 @@ + +$aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa$bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"good* +{"food":5}2media:1 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/reviews_submit_review_resp.pb b/plugin/wasmguest/caps/testdata/golden/reviews_submit_review_resp.pb new file mode 100644 index 0000000..fd4fe73 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/reviews_submit_review_resp.pb @@ -0,0 +1,2 @@ + +rev-1 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/settings_get_plugin_settings_req.pb b/plugin/wasmguest/caps/testdata/golden/settings_get_plugin_settings_req.pb new file mode 100644 index 0000000..d592105 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/settings_get_plugin_settings_req.pb @@ -0,0 +1,2 @@ + + symposium \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/settings_get_plugin_settings_resp.pb b/plugin/wasmguest/caps/testdata/golden/settings_get_plugin_settings_resp.pb new file mode 100644 index 0000000..78bd4e9 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/settings_get_plugin_settings_resp.pb @@ -0,0 +1,2 @@ + +{"enabled":true} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/settings_get_site_settings_req.pb b/plugin/wasmguest/caps/testdata/golden/settings_get_site_settings_req.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/settings_get_site_settings_resp.pb b/plugin/wasmguest/caps/testdata/golden/settings_get_site_settings_resp.pb new file mode 100644 index 0000000..21ce717 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/settings_get_site_settings_resp.pb @@ -0,0 +1,2 @@ + +&{"site_name":"Fixture","theme":"dark"} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_req.pb b/plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_req.pb new file mode 100644 index 0000000..e19fe85 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_req.pb @@ -0,0 +1,2 @@ + +myplugin{"enabled":true} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_resp.pb b/plugin/wasmguest/caps/testdata/golden/settings_update_plugin_settings_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/settings_update_site_setting_req.pb b/plugin/wasmguest/caps/testdata/golden/settings_update_site_setting_req.pb new file mode 100644 index 0000000..43b4e67 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/settings_update_site_setting_req.pb @@ -0,0 +1,2 @@ + +theme"light" \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/settings_update_site_setting_resp.pb b/plugin/wasmguest/caps/testdata/golden/settings_update_site_setting_resp.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_get_tier_by_slug_req.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_tier_by_slug_req.pb new file mode 100644 index 0000000..57c446c --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_tier_by_slug_req.pb @@ -0,0 +1,2 @@ + +gold \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_get_tier_by_slug_resp.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_tier_by_slug_resp.pb new file mode 100644 index 0000000..f1f05db --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_tier_by_slug_resp.pb @@ -0,0 +1,3 @@ + +? +$77777777-7777-7777-7777-777777777777Goldgold *d2{}8@ \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_get_user_tier_level_req.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_user_tier_level_req.pb new file mode 100644 index 0000000..1bdd816 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_user_tier_level_req.pb @@ -0,0 +1,2 @@ + +$11111111-1111-1111-1111-111111111111 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_get_user_tier_level_resp.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_user_tier_level_resp.pb new file mode 100644 index 0000000..d52f5e6 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/subscriptions_get_user_tier_level_resp.pb @@ -0,0 +1,2 @@ + + {"a":1} \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_list_active_plans_req.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_list_active_plans_req.pb new file mode 100644 index 0000000..589f9a3 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/subscriptions_list_active_plans_req.pb @@ -0,0 +1,2 @@ + +$77777777-7777-7777-7777-777777777777 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_list_active_plans_resp.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_list_active_plans_resp.pb new file mode 100644 index 0000000..915fe20 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/subscriptions_list_active_plans_resp.pb @@ -0,0 +1,3 @@ + +e +$88888888-8888-8888-8888-888888888888$77777777-7777-7777-7777-777777777777month &*usd0:Ȟ \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_list_tiers_req.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_list_tiers_req.pb new file mode 100644 index 0000000..e69de29 diff --git a/plugin/wasmguest/caps/testdata/golden/subscriptions_list_tiers_resp.pb b/plugin/wasmguest/caps/testdata/golden/subscriptions_list_tiers_resp.pb new file mode 100644 index 0000000..41e8fbc --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/subscriptions_list_tiers_resp.pb @@ -0,0 +1,3 @@ + +4 +$77777777-7777-7777-7777-777777777777Goldgold  \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/users_get_by_id_req.pb b/plugin/wasmguest/caps/testdata/golden/users_get_by_id_req.pb new file mode 100644 index 0000000..1bdd816 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/users_get_by_id_req.pb @@ -0,0 +1,2 @@ + +$11111111-1111-1111-1111-111111111111 \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/users_get_by_id_resp.pb b/plugin/wasmguest/caps/testdata/golden/users_get_by_id_resp.pb new file mode 100644 index 0000000..f7af707 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/users_get_by_id_resp.pb @@ -0,0 +1,3 @@ + +) +$11111111-1111-1111-1111-111111111111u \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/users_get_by_username_req.pb b/plugin/wasmguest/caps/testdata/golden/users_get_by_username_req.pb new file mode 100644 index 0000000..5fac564 --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/users_get_by_username_req.pb @@ -0,0 +1,2 @@ + +u \ No newline at end of file diff --git a/plugin/wasmguest/caps/testdata/golden/users_get_by_username_resp.pb b/plugin/wasmguest/caps/testdata/golden/users_get_by_username_resp.pb new file mode 100644 index 0000000..5d3a76a --- /dev/null +++ b/plugin/wasmguest/caps/testdata/golden/users_get_by_username_resp.pb @@ -0,0 +1,3 @@ + +E +$11111111-1111-1111-1111-111111111111u@x.comu"U*a2b8Bmember \ No newline at end of file diff --git a/plugin/wasmguest/caps/users.go b/plugin/wasmguest/caps/users.go new file mode 100644 index 0000000..e572f6a --- /dev/null +++ b/plugin/wasmguest/caps/users.go @@ -0,0 +1,47 @@ +package caps + +import ( + "context" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/auth" + "github.com/google/uuid" +) + +// usersStub implements auth.PublicUsers over users.* capability calls. +type usersStub struct{ base } + +var _ auth.PublicUsers = (*usersStub)(nil) + +func (s *usersStub) GetByUsername(ctx context.Context, username string) (*auth.PublicUserProfile, error) { + resp := &abiv1.UsersGetByUsernameResponse{} + req := &abiv1.UsersGetByUsernameRequest{Username: username} + if err := s.invoke(ctx, "get_by_username", req, resp); err != nil { + return nil, err + } + return publicUserFromProto(resp.GetUser()), nil +} + +func (s *usersStub) GetByID(ctx context.Context, id uuid.UUID) (*auth.PublicUserProfile, error) { + resp := &abiv1.UsersGetByIdResponse{} + if err := s.invoke(ctx, "get_by_id", &abiv1.UsersGetByIdRequest{Id: id.String()}, resp); err != nil { + return nil, err + } + return publicUserFromProto(resp.GetUser()), nil +} + +func publicUserFromProto(u *abiv1.PublicUserProfile) *auth.PublicUserProfile { + if u == nil { + return nil + } + return &auth.PublicUserProfile{ + ID: parseUUID(u.GetId()), + Email: u.GetEmail(), + Username: u.GetUsername(), + DisplayName: u.GetDisplayName(), + AvatarURL: u.GetAvatarUrl(), + Bio: u.GetBio(), + EmailVerified: u.GetEmailVerified(), + Role: u.GetRole(), + } +} diff --git a/plugin/wasmguest/caps/util.go b/plugin/wasmguest/caps/util.go new file mode 100644 index 0000000..260fec8 --- /dev/null +++ b/plugin/wasmguest/caps/util.go @@ -0,0 +1,34 @@ +package caps + +import ( + "encoding/json" + + "github.com/google/uuid" +) + +// parseUUID parses a canonical UUID string, returning uuid.Nil for empty or +// malformed input (host-side values are always canonical; this mirrors the +// tolerant decode the render-context shim uses). +func parseUUID(s string) uuid.UUID { + if s == "" { + return uuid.Nil + } + id, err := uuid.Parse(s) + if err != nil { + return uuid.Nil + } + return id +} + +// unmarshalMap decodes a JSON object into map[string]any, returning nil for +// empty or undecodable input (callers treat absent maps as nil). +func unmarshalMap(raw []byte) map[string]any { + if len(raw) == 0 { + return nil + } + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + return nil + } + return m +} diff --git a/plugin/wasmguest/caps_wasmhost_test.go b/plugin/wasmguest/caps_wasmhost_test.go new file mode 100644 index 0000000..04be9b8 --- /dev/null +++ b/plugin/wasmguest/caps_wasmhost_test.go @@ -0,0 +1,118 @@ +package wasmguest + +// End-to-end proof that the guest capability stubs (package caps, wired into +// CoreServices by WO-WZ-003) marshal to the ABI, cross into a host over the +// generic host_call import, and return decoded values the plugin's unchanged +// service code consumes — all inside a real wazero-compiled wasm module. +// +// The host_call handler below is a throwaway fake capability table (like the +// WO-WZ-002 throwaway host), not the cms host. It answers exactly the three +// families the fixture's Load hook exercises. + +import ( + "context" + "testing" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "google.golang.org/protobuf/proto" +) + +// installBlockninjaHost instantiates the "blockninja" import module exporting +// host_call, mirroring the guest calling convention in reverse (read a +// HostCallRequest from guest memory, answer it, write a HostCallResponse back +// via the guest's bn_alloc, return the packed pointer). +func installBlockninjaHost(t *testing.T, ctx context.Context, r wazero.Runtime) { + t.Helper() + _, err := r.NewHostModuleBuilder("blockninja"). + NewFunctionBuilder(). + WithFunc(func(ctx context.Context, mod api.Module, ptr, size uint32) uint64 { + data, ok := mod.Memory().Read(ptr, size) + if !ok { + return 0 + } + req := &abiv1.HostCallRequest{} + if err := proto.Unmarshal(data, req); err != nil { + return 0 + } + out, err := proto.Marshal(fakeCapability(req.GetMethod(), req.GetPayload())) + if err != nil { + return 0 + } + // Allocate the response in guest memory (bn_alloc may grow memory, + // so req is already decoded — data is no longer referenced). + res, err := mod.ExportedFunction("bn_alloc").Call(ctx, uint64(len(out))) + if err != nil || len(res) == 0 || res[0] == 0 { + return 0 + } + respPtr := uint32(res[0]) + if !mod.Memory().Write(respPtr, out) { + return 0 + } + return uint64(respPtr)<<32 | uint64(uint32(len(out))) + }). + Export("host_call"). + Instantiate(ctx) + if err != nil { + t.Fatalf("instantiate blockninja host module: %v", err) + } +} + +// fakeCapability answers the three capability methods the fixture Load hook +// calls; everything else is UNIMPLEMENTED. +func fakeCapability(method string, payload []byte) *abiv1.HostCallResponse { + pack := func(m proto.Message) *abiv1.HostCallResponse { + b, err := proto.Marshal(m) + if err != nil { + return &abiv1.HostCallResponse{Error: &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, Message: err.Error(), + }} + } + return &abiv1.HostCallResponse{Payload: b} + } + switch method { + case "content.get_page": + return pack(&abiv1.ContentGetPageResponse{Page: &abiv1.PageInfo{Slug: "about", Title: "About Us"}}) + case "settings.get_site_settings": + return pack(&abiv1.SettingsGetSiteSettingsResponse{SettingsJson: []byte(`{"site_name":"Fixture Site"}`)}) + case "bridge.get_service": + return pack(&abiv1.BridgeGetServiceResponse{Available: false}) + default: + return &abiv1.HostCallResponse{Error: &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, Message: "no fake for " + method, + }} + } +} + +// TestWasmFixtureCapabilityRoundTrip drives HOOK_LOAD (which calls the +// capability stubs) then renders the report block that surfaces the results, +// proving deps.Content / deps.Settings / deps.Bridge cross the ABI end-to-end. +func TestWasmFixtureCapabilityRoundTrip(t *testing.T) { + if testing.Short() { + t.Skip("compiles a wasm module; skipped in -short") + } + w := instantiateFixture(t) + + resp := w.invoke(t, abiv1.Hook_HOOK_LOAD, &abiv1.LoadRequest{}) + if resp.GetError() != nil { + t.Fatalf("LOAD error: %v", resp.GetError()) + } + + resp = w.invoke(t, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "wasmfixture:greeting", + ContentJson: []byte(`{"report":true}`), + }) + if resp.GetError() != nil { + t.Fatalf("RENDER_BLOCK error: %v", resp.GetError()) + } + rb := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rb); err != nil { + t.Fatalf("unmarshal RenderBlockResponse: %v", err) + } + const want = "

capreport:page=About Us site=Fixture Site bridge_nil=true

" + if rb.GetHtml() != want { + t.Errorf("capability report html = %q, want %q", rb.GetHtml(), want) + } + t.Logf("capability round trip through wasm: %s", rb.GetHtml()) +} diff --git a/plugin/wasmguest/context.go b/plugin/wasmguest/context.go new file mode 100644 index 0000000..6a1041d --- /dev/null +++ b/plugin/wasmguest/context.go @@ -0,0 +1,225 @@ +package wasmguest + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "github.com/google/uuid" +) + +// contextFromRenderContext rebuilds the context.Context that block and +// template code expects, applying each RenderContext field through the same +// context keys core/blocks/context.go defines. Existing render code reading +// via blocks.Get* works unchanged inside the guest. +// +// Function-valued context entries (SlotRenderer, MediaResolver, +// EmbedResolver, Queries) do not cross the wire; their v1 mappings are +// documented in core/docs/wasm-abi.md. RenderContext.theme_json and .locale +// have no core/blocks context key yet and are ignored here until a consumer +// key exists. +func contextFromRenderContext(ctx context.Context, rc *abiv1.RenderContext) context.Context { + if rc == nil { + return ctx + } + + if req := requestFromInfo(rc.GetRequest()); req != nil { + ctx = blocks.WithRequest(ctx, req) + } + if bc := blockContextFromProto(rc.GetBlockContext()); bc != nil { + ctx = blocks.WithBlockContext(ctx, bc) + } + if rc.GetTemplateKey() != "" { + ctx = blocks.WithTemplateKey(ctx, rc.GetTemplateKey()) + } + if p := rc.GetPage(); p != nil { + ctx = blocks.WithCurrentPage(ctx, &blocks.PageContext{ + ID: parseUUID(p.GetId()), + Slug: p.GetSlug(), + Title: p.GetTitle(), + PostType: p.GetPostType(), + Status: p.GetStatus(), + }) + } + if p := rc.GetPost(); p != nil { + post := &blocks.PostContext{ + ID: parseUUID(p.GetId()), + Slug: p.GetSlug(), + Title: p.GetTitle(), + Excerpt: p.GetExcerpt(), + FeaturedImageURL: p.GetFeaturedImageUrl(), + AuthorID: parseUUID(p.GetAuthorId()), + ReadingTime: int(p.GetReadingTime()), + IsFeatured: p.GetIsFeatured(), + } + if ts := p.GetPublishedAt(); ts != nil { + post.PublishedAt = ts.AsTime() + } + ctx = blocks.WithCurrentBlogPost(ctx, post) + } + if a := rc.GetAuthor(); a != nil { + ctx = blocks.WithCurrentAuthor(ctx, &blocks.AuthorContext{ + ID: parseUUID(a.GetId()), + Name: a.GetName(), + Slug: a.GetSlug(), + Bio: a.GetBio(), + AvatarURL: a.GetAvatarUrl(), + }) + } + if c := rc.GetCategory(); c != nil { + ctx = blocks.WithCurrentCategory(ctx, &blocks.CategoryContext{ + ID: parseUUID(c.GetId()), + Name: c.GetName(), + Slug: c.GetSlug(), + }) + } + if mp := rc.GetMasterPage(); mp != nil { + ctx = blocks.WithMasterPage(ctx, &blocks.MasterPageContext{ + ID: parseUUID(mp.GetId()), + Slug: mp.GetSlug(), + Title: mp.GetTitle(), + }) + } + if rc.GetRequestedPath() != "" { + ctx = blocks.WithRequestedPath(ctx, rc.GetRequestedPath()) + } + if slots := rc.GetInjectedSlots(); len(slots) > 0 { + ctx = blocks.WithInjectedSlots(ctx, slots) + } + if rc.GetIsEditor() { + ctx = blocks.WithIsEditor(ctx, true) + } + if slots := rc.GetExpectedSlots(); len(slots) > 0 { + ctx = blocks.WithExpectedSlots(ctx, slots) + } + if id := parseUUID(rc.GetBlockId()); id != uuid.Nil { + ctx = blocks.WithBlockID(ctx, id) + } + if id := parseUUID(rc.GetCurrentPageId()); id != uuid.Nil { + ctx = blocks.WithCurrentPageID(ctx, id) + } + if hp := rc.GetHumanProofBanner(); hp != nil { + ctx = blocks.WithHumanProofBanner(ctx, &blocks.HumanProofBannerData{ + ActiveTimeMinutes: int(hp.GetActiveTimeMinutes()), + KeystrokeCount: int(hp.GetKeystrokeCount()), + SessionCount: int(hp.GetSessionCount()), + PostSlug: hp.GetPostSlug(), + }) + } + if dr := rc.GetDetailRow(); dr != nil { + ctx = blocks.WithDetailRow(ctx, dr.GetTableId(), dr.GetRowId(), unmarshalMap(dr.GetDataJson())) + } + return ctx +} + +// requestFromInfo rebuilds a *http.Request carrying the subset of fields +// render code reads via blocks.GetRequest. +func requestFromInfo(ri *abiv1.RequestInfo) *http.Request { + if ri == nil { + return nil + } + u, err := url.Parse(ri.GetUrl()) + if err != nil || ri.GetUrl() == "" { + u = &url.URL{Path: ri.GetPath(), RawQuery: ri.GetRawQuery()} + } + req := &http.Request{ + Method: ri.GetMethod(), + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header, len(ri.GetHeaders())+2), + Host: ri.GetHost(), + RemoteAddr: ri.GetRemoteIp(), + RequestURI: u.RequestURI(), + } + for k, v := range ri.GetHeaders() { + req.Header.Set(k, v) + } + if ri.GetReferrer() != "" && req.Header.Get("Referer") == "" { + req.Header.Set("Referer", ri.GetReferrer()) + } + if ri.GetUserAgent() != "" && req.Header.Get("User-Agent") == "" { + req.Header.Set("User-Agent", ri.GetUserAgent()) + } + for name, value := range ri.GetCookies() { + req.AddCookie(&http.Cookie{Name: name, Value: value}) + } + return req +} + +func blockContextFromProto(pb *abiv1.BlockContext) *blocks.BlockContext { + if pb == nil { + return nil + } + bc := &blocks.BlockContext{ + URL: pb.GetUrl(), + Path: pb.GetPath(), + Slug: pb.GetSlug(), + PageID: pb.GetPageId(), + PageTitle: pb.GetPageTitle(), + TemplateKey: pb.GetTemplateKey(), + IsEditor: pb.GetIsEditor(), + Timestamp: pb.GetTimestamp(), + IsLoggedIn: pb.GetIsLoggedIn(), + UserID: pb.GetUserId(), + UserEmail: pb.GetUserEmail(), + UserRole: pb.GetUserRole(), + Method: pb.GetMethod(), + Host: pb.GetHost(), + Query: pb.GetQuery(), + Referrer: pb.GetReferrer(), + UserAgent: pb.GetUserAgent(), + IP: pb.GetIp(), + Cookies: pb.GetCookies(), + Headers: pb.GetHeaders(), + Country: pb.GetCountry(), + City: pb.GetCity(), + Timezone: pb.GetTimezone(), + CurrentAuthor: unmarshalMap(pb.GetCurrentAuthorJson()), + CurrentPost: unmarshalMap(pb.GetCurrentPostJson()), + CurrentCategory: unmarshalMap(pb.GetCurrentCategoryJson()), + Site: unmarshalMap(pb.GetSiteJson()), + IsPublicLoggedIn: pb.GetIsPublicLoggedIn(), + PublicUserID: pb.GetPublicUserId(), + PublicUsername: pb.GetPublicUsername(), + PublicDisplayName: pb.GetPublicDisplayName(), + PublicEmailVerified: pb.GetPublicEmailVerified(), + DetailRowID: pb.GetDetailRowId(), + DetailTableID: pb.GetDetailTableId(), + DetailRowData: unmarshalMap(pb.GetDetailRowDataJson()), + BlogIndexURL: pb.GetBlogIndexUrl(), + CategoryPageURL: pb.GetCategoryPageUrl(), + } + if ts := pb.GetNow(); ts != nil { + bc.Now = ts.AsTime() + } + return bc +} + +// unmarshalMap decodes a JSON object into map[string]any, returning nil for +// empty or undecodable input (render code treats absent maps as nil). +func unmarshalMap(raw []byte) map[string]any { + if len(raw) == 0 { + return nil + } + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + return nil + } + return m +} + +func parseUUID(s string) uuid.UUID { + if s == "" { + return uuid.Nil + } + id, err := uuid.Parse(s) + if err != nil { + return uuid.Nil + } + return id +} diff --git a/plugin/wasmguest/describe.go b/plugin/wasmguest/describe.go new file mode 100644 index 0000000..266fa13 --- /dev/null +++ b/plugin/wasmguest/describe.go @@ -0,0 +1,332 @@ +package wasmguest + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "connectrpc.com/connect" + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps" + "git.dev.alexdunmow.com/block/pluginsdk/rbac" + "github.com/google/uuid" + "google.golang.org/protobuf/proto" +) + +// AbiVersion is the ABI major version this guest shim implements +// (PluginManifest.abi_version). See core/docs/wasm-abi.md for the +// compatibility rules. +const AbiVersion uint32 = 1 + +// describe handles HOOK_DESCRIBE: validate the caller's ABI version and +// return the static PluginManifest. Publish-time only — never called on a +// live instance. +func (g *guest) describe(payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.DescribeRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("DescribeRequest", err) + } + // Symmetric version gate: a newer guest shim refuses a host it does not + // support, mirroring the host's manifest rejection. + if req.GetHostAbiVersion() != AbiVersion { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: fmt.Sprintf("guest implements ABI major version %d; host declared %d", + AbiVersion, req.GetHostAbiVersion()), + } + } + return &abiv1.DescribeResponse{Manifest: g.buildManifest()}, nil +} + +// buildManifest assembles the PluginManifest from the registration's static +// funcs plus the Register captures. Field-by-field mapping: +// core/docs/wasm-abi.md §"Manifest ↔ PluginRegistration mapping". +func (g *guest) buildManifest() *abiv1.PluginManifest { + reg := g.reg + m := &abiv1.PluginManifest{ + AbiVersion: AbiVersion, + Name: reg.Name, + Version: reg.Version, + HasHttpHandler: reg.HTTPHandler != nil, + HasLoadHook: reg.Load != nil, + HasUnloadHook: reg.Unload != nil, + HasMediaHooks: reg.MediaHooks != nil, + HasProvisioner: reg.RegisterWithProvisioner != nil, + } + + for _, d := range reg.Dependencies { + m.Dependencies = append(m.Dependencies, &abiv1.Dependency{ + Plugin: d.Plugin, MinVersion: d.MinVersion, Required: d.Required, + }) + } + + // Register captures. BlockMeta.Source is omitted deliberately — the host + // assigns it from the manifest name at load time. + for _, meta := range g.blocks.metas { + m.Blocks = append(m.Blocks, &abiv1.BlockMeta{ + Key: meta.Key, + Title: meta.Title, + Description: meta.Description, + Category: string(meta.Category), + HasInternalSlot: meta.HasInternalSlot, + Hidden: meta.Hidden, + EditorJs: meta.EditorJS, + }) + } + for _, o := range g.blocks.overrides { + m.BlockTemplateOverrides = append(m.BlockTemplateOverrides, &abiv1.BlockTemplateOverride{ + TemplateKey: o.templateKey, BlockKey: o.blockKey, Source: o.source, + }) + } + m.TemplateKeys = append(m.TemplateKeys, g.templates.templateKeys...) + for _, st := range g.templates.systemTemplates { + m.SystemTemplates = append(m.SystemTemplates, &abiv1.SystemTemplateMeta{ + Key: st.Key, Title: st.Title, Description: st.Description, + }) + } + for _, pt := range g.templates.pageTemplates { + m.PageTemplates = append(m.PageTemplates, &abiv1.PageTemplateMeta{ + SystemKey: pt.systemKey, + Key: pt.meta.Key, + Title: pt.meta.Title, + Description: pt.meta.Description, + Slots: pt.meta.Slots, + }) + } + seenWrapper := make(map[string]bool) + for _, ew := range g.templates.emailWrappers { + if !seenWrapper[ew.systemKey] { + seenWrapper[ew.systemKey] = true + m.EmailWrapperSystemKeys = append(m.EmailWrapperSystemKeys, ew.systemKey) + } + } + + // Static funcs. + if reg.AdminPages != nil { + for _, p := range reg.AdminPages() { + m.AdminPages = append(m.AdminPages, &abiv1.AdminPage{ + Key: p.Key, Title: p.Title, Icon: p.Icon, Route: p.Route, + }) + } + } + if reg.SettingsSchema != nil { + m.SettingsSchema = reg.SettingsSchema() + } + if reg.ThemePresets != nil { + m.ThemePresets = reg.ThemePresets() + } + if reg.BundledFonts != nil { + m.BundledFonts = reg.BundledFonts() + } + if reg.MasterPages != nil { + for _, mp := range reg.MasterPages() { + m.MasterPages = append(m.MasterPages, masterPageToProto(mp)) + } + } + if reg.AIActions != nil { + for _, a := range reg.AIActions() { + m.AiActions = append(m.AiActions, &abiv1.AiAction{ + Key: a.Key, + Title: a.Title, + Description: a.Description, + DefaultProvider: a.DefaultProvider, + DefaultModel: a.DefaultModel, + }) + } + } + if reg.CSSManifest != nil { + if cm := reg.CSSManifest(); cm != nil { + m.CssManifest = &abiv1.CssManifest{ + NpmPackages: cm.NPMPackages, + CssDirectives: cm.CSSDirectives, + InputCssAppend: cm.InputCSSAppend, + } + } + } + m.RequiredIconPacks = append(m.RequiredIconPacks, reg.RequiredIconPacks...) + + // Custom template tags/filters the plugin registered via blocks.RegisterTag + // / blocks.RegisterFilter during the Register pass (runRegister reset the + // registry first, so these reflect exactly this plugin). The host wires a + // pongo2 tag/filter per name that calls back via HOOK_RENDER_TAG / + // HOOK_APPLY_FILTER. + m.DeclaredTags = blocks.RegisteredTagNames() + m.DeclaredFilters = blocks.RegisteredFilterNames() + if reg.DirectoryExtensions != nil { + if de := reg.DirectoryExtensions(); de != nil { + pde := &abiv1.DirectoryExtensions{ + BooleanFilterFields: de.BooleanFilterFields, + SelectFilterFields: de.SelectFilterFields, + PanelSectionCount: uint32(len(de.PanelSections)), + PinDecoratorCount: uint32(len(de.PinDecorators)), + } + if len(de.BadgeLabels) > 0 { + pde.BadgeLabels = make(map[string]*abiv1.BadgeLabel, len(de.BadgeLabels)) + for k, v := range de.BadgeLabels { + pde.BadgeLabels[k] = &abiv1.BadgeLabel{Positive: v[0], Negative: v[1]} + } + } + m.DirectoryExtensions = pde + } + } + if reg.SettingsPanel != nil { + m.SettingsPanel = reg.SettingsPanel() + } + + // Function fields probed with capture-only CoreServices. These calls run + // without a live host, so panics from touching unwired capabilities are + // swallowed — whatever was declared before the panic is still captured. + m.JobTypes = g.captureJobTypes() + m.RagContentFetcherTypes = g.captureRagFetcherTypes() + g.captureServiceRegistration(m) + + return m +} + +// captureJobTypes enumerates JobHandlers keys with describe-time services. +func (g *guest) captureJobTypes() (types []string) { + if g.reg.JobHandlers == nil { + return nil + } + defer func() { recover() }() //nolint:errcheck // publish-time probe; partial capture is intended + handlers := g.reg.JobHandlers(describeServices()) + types = sortedKeys(handlers) + return types +} + +// captureRagFetcherTypes runs Load against capture-only services to record +// RAGService.RegisterContentFetcher declarations. DESCRIBE is publish-time +// only, so Load's runtime side effects have no host to land on; a panic +// (e.g. from an unwired capability) ends the probe but keeps prior +// registrations. +func (g *guest) captureRagFetcherTypes() (types []string) { + if g.reg.Load == nil { + return nil + } + rag := caps.NewRAGStub(nil) + svcs := describeServices() + svcs.RAGService = rag + func() { + defer func() { recover() }() //nolint:errcheck // publish-time probe + _ = g.reg.Load(svcs) + }() + return rag.FetcherTypes() +} + +// captureServiceRegistration probes ServiceHandlers to record RBAC method +// roles and CoreServiceBindings.Bind declarations. +func (g *guest) captureServiceRegistration(m *abiv1.PluginManifest) { + if g.reg.ServiceHandlers == nil { + return + } + bindings := &captureCoreServiceBindings{} + svcs := describeServices() + svcs.CoreServiceBindings = bindings + + var sr *plugin.ServiceRegistration + func() { + defer func() { recover() }() //nolint:errcheck // publish-time probe + reg, err := g.reg.ServiceHandlers(svcs) + if err == nil { + sr = reg + } + }() + + m.CoreServiceBindings = append(m.CoreServiceBindings, bindings.bound...) + if sr == nil { + return + } + coreBound := make(map[string]bool, len(bindings.bound)) + for _, b := range bindings.bound { + coreBound[b.GetServiceName()] = true + } + for _, svc := range sr.Services { + if coreBound[svc.Name()] { + continue // declared via core_service_bindings, host mounts it + } + for method, role := range svc.MethodRoles() { + if m.RbacMethodRoles == nil { + m.RbacMethodRoles = make(map[string]string) + } + m.RbacMethodRoles[method] = string(role) + } + } +} + +// describeServices returns the CoreServices value used to probe registration +// funcs at publish time. It uses the capability stubs with a nil transport: +// there is no live host during DESCRIBE, so any capability a plugin invokes +// returns a clean "no host transport" error instead of nil-panicking, letting +// the probe capture whatever static registrations (jobs, RAG fetchers) +// surround the call. Callers still recover, belt-and-suspenders. +func describeServices() plugin.CoreServices { + return caps.NewCoreServices(nil) +} + +func masterPageToProto(mp plugin.MasterPageDefinition) *abiv1.MasterPageDefinition { + out := &abiv1.MasterPageDefinition{ + Key: mp.Key, + Title: mp.Title, + PageTemplates: mp.PageTemplates, + } + for _, b := range mp.Blocks { + pb := &abiv1.MasterPageBlock{ + BlockKey: b.BlockKey, + Title: b.Title, + Slot: b.Slot, + SortOrder: b.SortOrder, + HtmlContent: b.HtmlContent, + } + if b.Content != nil { + if raw, err := json.Marshal(b.Content); err == nil { + pb.ContentJson = raw + } + } + out.Blocks = append(out.Blocks, pb) + } + return out +} + +// --- capture / no-op stubs used only during DESCRIBE probes --- + +type captureCoreServiceBindings struct { + bound []*abiv1.CoreServiceBinding +} + +func (c *captureCoreServiceBindings) Bind(serviceName string, methodRoles map[string]rbac.Role) (plugin.ConnectServiceBinding, error) { + roles := make(map[string]string, len(methodRoles)) + for k, v := range methodRoles { + roles[k] = string(v) + } + c.bound = append(c.bound, &abiv1.CoreServiceBinding{ServiceName: serviceName, MethodRoles: roles}) + return plugin.NewConnectServiceBinding(serviceName, struct{}{}, + func(_ struct{}, _ ...connect.HandlerOption) (string, http.Handler) { + return "/" + serviceName + "/", http.NotFoundHandler() + }, methodRoles), nil +} + +// noopProvisioner satisfies plugin.Provisioner for the DESCRIBE-time +// Register pass; real provisioning runs host-side at load. +type noopProvisioner struct{} + +func (noopProvisioner) EnsureDataTable(plugin.DataTableConfig) error { return nil } +func (noopProvisioner) MergeSiteSettings(map[string]any) error { return nil } +func (noopProvisioner) EnsureSetting(string, any) error { return nil } +func (noopProvisioner) EnsurePage(plugin.PageConfig) error { return nil } +func (noopProvisioner) OverrideSiteSettings(map[string]any) error { return nil } +func (noopProvisioner) EnsureMenuItem(string, plugin.MenuItemConfig) error { return nil } +func (noopProvisioner) RegisterEmbeddingConfig(plugin.EmbeddingConfigDef) error { + return nil +} +func (noopProvisioner) EnsureEmbed(plugin.EmbedConfig) error { return nil } +func (noopProvisioner) EnsureJobSchedule(plugin.JobScheduleConfig) error { return nil } +func (noopProvisioner) UpdateDataTableRowField(context.Context, uuid.UUID, string, any) error { + return nil +} +func (noopProvisioner) DisableOrphanedJobSchedules([]string) error { return nil } +func (noopProvisioner) EnsurePlugin(string) error { return nil } +func (noopProvisioner) EnsureCustomColor(plugin.CustomColorConfig) error { return nil } +func (noopProvisioner) EnsureMedia(plugin.MediaDeposit) error { return nil } diff --git a/plugin/wasmguest/dispatch.go b/plugin/wasmguest/dispatch.go new file mode 100644 index 0000000..3cd419b --- /dev/null +++ b/plugin/wasmguest/dispatch.go @@ -0,0 +1,720 @@ +package wasmguest + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "sort" + "time" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/ai" + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm" + "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps" + "github.com/google/uuid" + "google.golang.org/protobuf/proto" +) + +// capTransport is the guest→host capability transport handed to the +// capability stubs (package caps). hostcalls.go (wasip1) binds it to CallHost in an init; +// on native builds it stays nil, so capability calls report "no host +// transport" rather than reaching a boundary that isn't there. Tests exercise +// the stubs directly with their own fake transport. +var capTransport caps.CallFunc + +// current is the guest runtime installed by Serve. The bn_invoke export +// (exports.go, wasip1) dispatches through it. +var current *guest + +// Serve installs a plugin registration as the wasm guest runtime. It is +// non-blocking: plugins call it from an init func (see doc.go) so it runs +// during the module's `_initialize`; all work then arrives via bn_invoke. +func Serve(reg plugin.PluginRegistration) { + current = newGuest(reg) +} + +// HostServices returns the CoreServices bound to THIS guest instance. Unlike +// the deps handed to Load/HTTPHandler/JobHandlers — which the host delivers on +// only one pooled instance (Load) or lazily on first use — these services are +// bound at `_initialize` on EVERY pooled instance (newGuest). The interface +// fields are host_call-backed capability stubs and Pool is a live db.* bridge. +// +// This is the escape hatch for entry points the ABI does not hand deps to — +// most importantly block/template render funcs (HOOK_RENDER_BLOCK gives the +// closure only ctx+content, no services). A DB-backed block reads its pool from +// HostServices().Pool so it works on any pooled instance, not just the one that +// happened to run Load. In a native build (Serve never called — no wasip1 +// _initialize) current is nil and this returns the zero CoreServices, so guest +// code must nil-check Pool (native tests inject a real pool by other means). +func HostServices() plugin.CoreServices { + if current == nil { + return plugin.CoreServices{} + } + return current.services +} + +// guest adapts a plugin.PluginRegistration to ABI hook calls. +type guest struct { + reg plugin.PluginRegistration + blocks *captureBlockRegistry + templates *captureTemplateRegistry + registerErr error + + // services is the CoreServices value handed to the registration's + // function fields (HTTPHandler, JobHandlers, Load). Its interface fields + // are caps host_call-backed stubs (package caps); LOAD fills + // AppURL/MediaPath from HostConfig. + services plugin.CoreServices + + // rag is the guest-side RAG stub inside services. It records + // RegisterContentFetcher calls (dispatched via HOOK_RAG_FETCH) while its + // Query/OnContentChanged marshal out as capability calls. + rag *caps.RAGStub + + jobHandlers map[string]plugin.JobHandlerFunc + jobHandlersInit bool + httpHandler http.Handler + httpHandlerInit bool +} + +// newGuest captures the registration's Register pass so block/template +// functions are addressable before any hook fires. +func newGuest(reg plugin.PluginRegistration) *guest { + g := &guest{ + reg: reg, + blocks: newCaptureBlockRegistry(), + templates: newCaptureTemplateRegistry(), + } + g.services = caps.NewCoreServices(capTransport) + // Pool is left zero by NewCoreServices (it does not cross as a capability + // call); bind the db.* driver over the same transport so deps.Pool keeps + // working for plugin sqlc code. A nil transport (native/DESCRIBE) yields a + // Pool whose calls fail cleanly, matching the capability stubs. + g.services.Pool = bnwasm.NewPool(bnwasm.Transport(capTransport)) + g.rag, _ = g.services.RAGService.(*caps.RAGStub) + g.registerErr = runRegister(reg, g.templates, g.blocks) + return g +} + +// runRegister invokes Register (or RegisterWithProvisioner with a no-op +// provisioner) against capture registries, converting panics to errors. +// Register-time provisioning cannot cross the ABI (DESCRIBE stubs every host +// function to fail), so RegisterWithProvisioner's provisioner is inert here; +// wasm plugins provision from Load via deps.Provisioner (the provisioner.* +// capability family, WO-WZ-019) instead. +func runRegister(reg plugin.PluginRegistration, tr *captureTemplateRegistry, br *captureBlockRegistry) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic in Register: %v", r) + } + }() + // Give this registration a clean tag/filter slate. blocks.RegisterTag / + // RegisterFilter write a package-level registry (one plugin owns the wasm + // module), so resetting here makes newGuest deterministic: after Register, + // the registry holds exactly this plugin's tags/filters for DESCRIBE and + // HOOK_RENDER_TAG / HOOK_APPLY_FILTER dispatch. + blocks.ResetRegisteredTagsAndFilters() + // Match the .so loader: block keys are plugin-prefixed and override + // sources stamped via PluginBlockRegistry. + pbr := plugin.NewPluginBlockRegistry(br, reg.Name) + switch { + case reg.Register != nil: + return reg.Register(tr, pbr) + case reg.RegisterWithProvisioner != nil: + return reg.RegisterWithProvisioner(tr, pbr, noopProvisioner{}) + default: + return nil + } +} + +// invoke handles one bn_invoke call: (hook id, serialized InvokeRequest) → +// serialized InvokeResponse. It never panics — plugin-level panics come back +// as ABI_ERROR_CODE_INTERNAL so the instance stays usable; traps are +// reserved for runtime corruption. +func (g *guest) invoke(hookID uint32, req []byte) (out []byte) { + defer func() { + if r := recover(); r != nil { + out = errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, fmt.Sprintf("panic: %v", r)) + } + }() + + env := &abiv1.InvokeRequest{} + if err := proto.Unmarshal(req, env); err != nil { + return errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE, "invalid InvokeRequest: "+err.Error()) + } + if uint32(env.GetHook()) != hookID { + return errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE, + fmt.Sprintf("hook id mismatch: export arg %d, envelope %d", hookID, env.GetHook())) + } + if g.registerErr != nil { + return errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, "plugin Register failed: "+g.registerErr.Error()) + } + + ctx := context.Background() + if d := env.GetDeadlineMs(); d > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(d)*time.Millisecond) + defer cancel() + } + + var ( + payload proto.Message + abiErr *abiv1.AbiError + ) + switch env.GetHook() { + case abiv1.Hook_HOOK_DESCRIBE: + payload, abiErr = g.describe(env.GetPayload()) + case abiv1.Hook_HOOK_RENDER_BLOCK: + payload, abiErr = g.renderBlock(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_RENDER_TEMPLATE: + payload, abiErr = g.renderTemplate(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_RENDER_TAG: + payload, abiErr = g.renderTag(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_APPLY_FILTER: + payload, abiErr = g.applyFilter(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_HANDLE_HTTP: + payload, abiErr = g.handleHTTP(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_JOB: + payload, abiErr = g.runJob(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_LOAD: + payload, abiErr = g.load(env.GetPayload()) + case abiv1.Hook_HOOK_UNLOAD: + payload, abiErr = g.unload(ctx) + case abiv1.Hook_HOOK_RAG_FETCH: + payload, abiErr = g.ragFetch(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_MEDIA_HOOK: + payload, abiErr = g.mediaHook(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_AI_TOOL_CALL: + payload, abiErr = g.aiToolCall(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_BRIDGE_CALL: + payload, abiErr = g.bridgeCall(ctx, env.GetPayload()) + case abiv1.Hook_HOOK_DIRECTORY_PANEL_SECTION: + payload, abiErr = g.directoryPanelSection(env.GetPayload()) + case abiv1.Hook_HOOK_DIRECTORY_PIN_DECORATOR: + payload, abiErr = g.directoryPinDecorator(env.GetPayload()) + default: + abiErr = &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: fmt.Sprintf("unknown hook %d", env.GetHook()), + } + } + + resp := &abiv1.InvokeResponse{Error: abiErr} + if abiErr == nil && payload != nil { + b, err := proto.Marshal(payload) + if err != nil { + return errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, "marshal response payload: "+err.Error()) + } + resp.Payload = b + } + b, err := proto.Marshal(resp) + if err != nil { + return errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, "marshal InvokeResponse: "+err.Error()) + } + return b +} + +// --- hook handlers --- + +func (g *guest) renderBlock(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.RenderBlockRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("RenderBlockRequest", err) + } + fn, ok := g.blocks.lookup(req.GetBlockKey(), req.GetRenderContext().GetTemplateKey()) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "unknown block key " + req.GetBlockKey(), + } + } + ctx = contextFromRenderContext(ctx, req.GetRenderContext()) + content := unmarshalMap(req.GetContentJson()) + if content == nil { + content = map[string]any{} + } + out := fn(ctx, content) + // A block may return EITHER final HTML or a "powered" result + // (blocks.PoweredBlock): a template + data the HOST renders with pongo2 + // after this call returns. Detect the powered marker and forward it as + // PoweredBlock; otherwise it is plain HTML. + if pr, ok := blocks.DecodePoweredBlock(out); ok { + dataJSON, err := json.Marshal(pr.Data) + if err != nil { + return nil, internalError("marshal powered block data: " + err.Error()) + } + return &abiv1.RenderBlockResponse{ + Powered: &abiv1.PoweredBlock{Template: pr.Template, DataJson: dataJSON}, + }, nil + } + return &abiv1.RenderBlockResponse{Html: out}, nil +} + +// renderTag handles HOOK_RENDER_TAG: the host engine hit a plugin-declared +// tag while rendering a powered block and calls back to run it. Re-entrancy- +// free — the originating RENDER_BLOCK has already returned, so this is a fresh +// invoke. A tag fn error surfaces in RenderTagResponse.error (not an AbiError, +// which stays reserved for decode/dispatch/panic failures). +func (g *guest) renderTag(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.RenderTagRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("RenderTagRequest", err) + } + fn, ok := blocks.LookupTag(req.GetTagName()) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "unknown tag " + req.GetTagName(), + } + } + ctx = contextFromRenderContext(ctx, req.GetRenderContext()) + args := unmarshalMap(req.GetArgsJson()) + if args == nil { + args = map[string]any{} + } + html, err := fn(args, ctx) + if err != nil { + return &abiv1.RenderTagResponse{Error: err.Error()}, nil + } + return &abiv1.RenderTagResponse{Html: html}, nil +} + +// applyFilter handles HOOK_APPLY_FILTER: run a plugin-declared filter. Same +// re-entrancy-free callback model as renderTag. +func (g *guest) applyFilter(_ context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.ApplyFilterRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("ApplyFilterRequest", err) + } + fn, ok := blocks.LookupFilter(req.GetFilterName()) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "unknown filter " + req.GetFilterName(), + } + } + args := unmarshalMap(req.GetArgsJson()) + if args == nil { + args = map[string]any{} + } + out, err := fn(req.GetInput(), args) + if err != nil { + return &abiv1.ApplyFilterResponse{Error: err.Error()}, nil + } + return &abiv1.ApplyFilterResponse{Output: out}, nil +} + +func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.RenderTemplateRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("RenderTemplateRequest", err) + } + fn, ok := g.templates.lookup(req.GetTemplateKey()) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "unknown template key " + req.GetTemplateKey(), + } + } + ctx = contextFromRenderContext(ctx, req.GetRenderContext()) + doc := unmarshalMap(req.GetDocJson()) + if doc == nil { + doc = map[string]any{} + } + component := fn(ctx, doc) + if component == nil { + return nil, internalError("template " + req.GetTemplateKey() + " returned nil component") + } + var buf bytes.Buffer + if err := component.Render(ctx, &buf); err != nil { + return nil, internalError("render template " + req.GetTemplateKey() + ": " + err.Error()) + } + return &abiv1.RenderTemplateResponse{Html: buf.Bytes()}, nil +} + +func (g *guest) handleHTTP(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + if g.reg.HTTPHandler == nil { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "plugin has no HTTP handler", + } + } + req := &abiv1.HttpRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("HttpRequest", err) + } + if !g.httpHandlerInit { + g.httpHandler = g.reg.HTTPHandler(g.services) + g.httpHandlerInit = true + } + if g.httpHandler == nil { + return nil, internalError("HTTPHandler returned nil handler") + } + + httpReq, err := http.NewRequestWithContext(ctx, req.GetMethod(), + (&url.URL{Path: req.GetPath(), RawQuery: req.GetRawQuery()}).String(), + bytes.NewReader(req.GetBody())) + if err != nil { + return nil, internalError("build http request: " + err.Error()) + } + for name, hv := range req.GetHeaders() { + for _, v := range hv.GetValues() { + httpReq.Header.Add(name, v) + } + } + + rec := httptest.NewRecorder() + g.httpHandler.ServeHTTP(rec, httpReq) + res := rec.Result() + + out := &abiv1.HttpResponse{ + Status: int32(res.StatusCode), + Headers: make(map[string]*abiv1.HeaderValues, len(res.Header)), + Body: rec.Body.Bytes(), + } + for name, values := range res.Header { + out.Headers[name] = &abiv1.HeaderValues{Values: values} + } + return out, nil +} + +func (g *guest) runJob(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.JobRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("JobRequest", err) + } + if !g.jobHandlersInit { + if g.reg.JobHandlers != nil { + g.jobHandlers = g.reg.JobHandlers(g.services) + } + g.jobHandlersInit = true + } + handler, ok := g.jobHandlers[req.GetJobType()] + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "unknown job type " + req.GetJobType(), + } + } + // Progress crosses as best-effort jobs.progress host calls; the host + // correlates them to the running job via the HOOK_JOB call's context. A + // nil transport (native builds) discards updates. + progress := func(current, total int, message string) { + if capTransport == nil { + return + } + preq := &abiv1.JobsProgressRequest{Current: int32(current), Total: int32(total), Message: message} + _ = capTransport("jobs.progress", preq, &abiv1.JobsProgressResponse{}) + } + result, err := handler(ctx, req.GetConfigJson(), progress) + if err != nil { + return nil, internalError(err.Error()) + } + return &abiv1.JobResponse{ResultJson: result}, nil +} + +func (g *guest) load(payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.LoadRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("LoadRequest", err) + } + if hc := req.GetHostConfig(); hc != nil { + g.services.AppURL = hc.GetAppUrl() + g.services.MediaPath = hc.GetMediaPath() + } + if g.reg.Load != nil { + if err := g.reg.Load(g.services); err != nil { + return nil, internalError(err.Error()) + } + } + return &abiv1.LoadResponse{}, nil +} + +func (g *guest) unload(ctx context.Context) (proto.Message, *abiv1.AbiError) { + if g.reg.Unload != nil { + if err := g.reg.Unload(ctx); err != nil { + return nil, internalError(err.Error()) + } + } + return &abiv1.UnloadResponse{}, nil +} + +func (g *guest) ragFetch(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.RagFetchRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("RagFetchRequest", err) + } + fetcher, ok := g.ragFetcher(req.GetContentType()) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "no content fetcher for type " + req.GetContentType(), + } + } + contentID, err := uuid.Parse(req.GetContentId()) + if err != nil { + return nil, decodeError("RagFetchRequest.content_id", err) + } + title, text, err := fetcher(ctx, contentID) + if err != nil { + return nil, internalError(err.Error()) + } + return &abiv1.RagFetchResponse{Title: title, Text: text}, nil +} + +func (g *guest) mediaHook(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + if g.reg.MediaHooks == nil { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "plugin has no media hooks", + } + } + req := &abiv1.MediaHookRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("MediaHookRequest", err) + } + var err error + switch ev := req.GetEvent().(type) { + case *abiv1.MediaHookRequest_MediaAnalyzed: + e := ev.MediaAnalyzed + err = g.reg.MediaHooks.OnMediaAnalyzed(ctx, plugin.MediaAnalyzedEvent{ + MediaID: parseUUID(e.GetMediaId()), + AnalysisID: parseUUID(e.GetAnalysisId()), + ContentHash: e.GetContentHash(), + Status: e.GetStatus(), + SourcePlugin: e.GetSourcePlugin(), + SourceType: e.GetSourceType(), + SourceRefID: parseUUID(e.GetSourceRefId()), + SafeAdult: e.GetSafeAdult(), + SafeViolence: e.GetSafeViolence(), + SafeRacy: e.GetSafeRacy(), + }) + case *abiv1.MediaHookRequest_ModerationDecision: + e := ev.ModerationDecision + err = g.reg.MediaHooks.OnModerationDecision(ctx, plugin.ModerationDecisionEvent{ + MediaID: parseUUID(e.GetMediaId()), + AnalysisID: parseUUID(e.GetAnalysisId()), + Status: e.GetStatus(), + PreviousStatus: e.GetPreviousStatus(), + SourcePlugin: e.GetSourcePlugin(), + SourceType: e.GetSourceType(), + SourceRefID: parseUUID(e.GetSourceRefId()), + ModeratedBy: parseUUID(e.GetModeratedBy()), + Note: e.GetNote(), + }) + default: + return nil, decodeError("MediaHookRequest.event", fmt.Errorf("empty oneof")) + } + if err != nil { + return nil, internalError(err.Error()) + } + return &abiv1.MediaHookResponse{}, nil +} + +// --- error helpers --- + +func internalError(msg string) *abiv1.AbiError { + return &abiv1.AbiError{Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, Message: msg} +} + +func decodeError(what string, err error) *abiv1.AbiError { + return &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE, + Message: "decode " + what + ": " + err.Error(), + } +} + +// errorResponse builds a serialized error-only InvokeResponse. Marshaling a +// flat AbiError cannot fail; if the runtime is corrupted enough that it +// does, returning nil makes bn_invoke return packed 0, which the host treats +// as INTERNAL + instance discard (per wasm-abi.md). +func errorResponse(code abiv1.AbiErrorCode, msg string) []byte { + b, err := proto.Marshal(&abiv1.InvokeResponse{Error: &abiv1.AbiError{Code: code, Message: msg}}) + if err != nil { + return nil + } + return b +} + +// ragFetcher returns the content fetcher the plugin registered for a content +// type (via RAGService.RegisterContentFetcher during LOAD), for +// HOOK_RAG_FETCH dispatch. Nil-safe when the guest has no RAG stub. +func (g *guest) ragFetcher(contentType string) (plugin.ContentFetcher, bool) { + if g.rag == nil { + return nil, false + } + return g.rag.Fetcher(contentType) +} + +// toolLookup / serviceLookup are the recording-stub accessors dispatch needs +// for HOOK_AI_TOOL_CALL / HOOK_BRIDGE_CALL. The caps stubs implement them; +// asserting interfaces keeps the stub types unexported. +type toolLookup interface { + Tool(slug string) (*ai.ToolDefinition, bool) +} + +type serviceLookup interface { + Service(serviceName string) (any, bool) +} + +// aiToolCall executes a guest-registered AI tool handler (HOOK_AI_TOOL_CALL). +// Tools must be registered in Register (every pooled instance runs it) for the +// lookup to succeed on any instance — see wasm-abi.md §"Per-instance state". +func (g *guest) aiToolCall(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.AiToolCallRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("AiToolCallRequest", err) + } + reg, ok := g.services.ToolRegistry.(toolLookup) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "tool registry does not record handlers", + } + } + tool, ok := reg.Tool(req.GetSlug()) + if !ok || tool.Handler == nil { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "no handler for AI tool " + req.GetSlug() + " on this instance (register tools in Register, not Load)", + } + } + var params map[string]any + if raw := req.GetParamsJson(); len(raw) > 0 { + if err := json.Unmarshal(raw, ¶ms); err != nil { + return nil, decodeError("AiToolCallRequest.params_json", err) + } + } + result, err := tool.Handler(ctx, params) + if err != nil { + return nil, internalError(err.Error()) + } + resp := &abiv1.AiToolCallResponse{} + if result != nil { + resp.Content = result.Content + resp.ErrorMessage = result.Error + } + return resp, nil +} + +// bridgeCall invokes a method on one of THIS plugin's registered bridge +// services (HOOK_BRIDGE_CALL; the consumer side is the bridge.invoke +// capability). The service value must implement plugin.BridgeInvokable and be +// registered in Register so every pooled instance has it. +func (g *guest) bridgeCall(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.BridgeCallRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("BridgeCallRequest", err) + } + lookup, ok := g.services.Bridge.(serviceLookup) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "bridge does not record services", + } + } + svc, ok := lookup.Service(req.GetServiceName()) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "no bridge service " + req.GetServiceName() + " on this instance (register services in Register, not Load)", + } + } + invokable, ok := svc.(plugin.BridgeInvokable) + if !ok { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: "bridge service " + req.GetServiceName() + " does not implement plugin.BridgeInvokable", + } + } + out, err := invokable.InvokeBridge(ctx, req.GetMethod(), req.GetPayload()) + if err != nil { + return nil, internalError(err.Error()) + } + return &abiv1.BridgeCallResponse{Payload: out}, nil +} + +// directoryExtensions resolves the registration's DirectoryExtensions value, +// nil when the plugin declares none. +func (g *guest) directoryExtensions() *plugin.DirectoryExtensions { + if g.reg.DirectoryExtensions == nil { + return nil + } + return g.reg.DirectoryExtensions() +} + +// directoryPanelSection renders the index-th registered panel section +// (HOOK_DIRECTORY_PANEL_SECTION). +func (g *guest) directoryPanelSection(payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.DirectoryPanelSectionRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("DirectoryPanelSectionRequest", err) + } + ext := g.directoryExtensions() + idx := int(req.GetIndex()) + if ext == nil || idx >= len(ext.PanelSections) || ext.PanelSections[idx] == nil { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: fmt.Sprintf("no directory panel section at index %d", idx), + } + } + var data map[string]any + if raw := req.GetDataJson(); len(raw) > 0 { + if err := json.Unmarshal(raw, &data); err != nil { + return nil, decodeError("DirectoryPanelSectionRequest.data_json", err) + } + } + return &abiv1.DirectoryPanelSectionResponse{Html: ext.PanelSections[idx](data)}, nil +} + +// directoryPinDecorator runs the index-th registered pin decorator, which +// mutates the pin map in place; the mutated pin is returned +// (HOOK_DIRECTORY_PIN_DECORATOR). +func (g *guest) directoryPinDecorator(payload []byte) (proto.Message, *abiv1.AbiError) { + req := &abiv1.DirectoryPinDecoratorRequest{} + if err := proto.Unmarshal(payload, req); err != nil { + return nil, decodeError("DirectoryPinDecoratorRequest", err) + } + ext := g.directoryExtensions() + idx := int(req.GetIndex()) + if ext == nil || idx >= len(ext.PinDecorators) || ext.PinDecorators[idx] == nil { + return nil, &abiv1.AbiError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED, + Message: fmt.Sprintf("no directory pin decorator at index %d", idx), + } + } + pin := map[string]any{} + if raw := req.GetPinJson(); len(raw) > 0 { + if err := json.Unmarshal(raw, &pin); err != nil { + return nil, decodeError("DirectoryPinDecoratorRequest.pin_json", err) + } + } + var data map[string]any + if raw := req.GetDataJson(); len(raw) > 0 { + if err := json.Unmarshal(raw, &data); err != nil { + return nil, decodeError("DirectoryPinDecoratorRequest.data_json", err) + } + } + ext.PinDecorators[idx](pin, data) + pinJSON, err := json.Marshal(pin) + if err != nil { + return nil, internalError("marshal mutated pin: " + err.Error()) + } + return &abiv1.DirectoryPinDecoratorResponse{PinJson: pinJSON}, nil +} + +// sortedKeys returns the map's keys in sorted order (deterministic manifests). +func sortedKeys[V any](m map[string]V) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/plugin/wasmguest/dispatch_test.go b/plugin/wasmguest/dispatch_test.go new file mode 100644 index 0000000..3e053a4 --- /dev/null +++ b/plugin/wasmguest/dispatch_test.go @@ -0,0 +1,371 @@ +package wasmguest + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + "testing" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "git.dev.alexdunmow.com/block/pluginsdk/templates" + "google.golang.org/protobuf/proto" +) + +// --- test fixture registration --- + +type textComponent string + +func (c textComponent) Render(_ context.Context, w io.Writer) error { + _, err := io.WriteString(w, string(c)) + return err +} + +func fixtureRegistration() plugin.PluginRegistration { + return plugin.PluginRegistration{ + Name: "fixture", + Version: "0.1.0", + Dependencies: []plugin.Dependency{ + {Plugin: "other", MinVersion: "1.2.3", Required: true}, + }, + Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error { + br.Register(blocks.BlockMeta{ + Key: "hello", + Title: "Hello Block", + Description: "Says hello", + Category: blocks.CategoryContent, + }, func(ctx context.Context, content map[string]any) string { + if boom, _ := content["boom"].(bool); boom { + panic("kaboom") + } + name, _ := content["name"].(string) + page := blocks.GetCurrentPage(ctx) + pageSlug := "" + if page != nil { + pageSlug = page.Slug + } + return fmt.Sprintf("

hello %s on %s (tpl %s)

", name, pageSlug, blocks.GetTemplateKey(ctx)) + }) + br.RegisterTemplateOverride("landing", "hello", func(ctx context.Context, content map[string]any) string { + return "

override hello

" + }) + tr.RegisterSystemTemplate(templates.SystemTemplateMeta{Key: "fixture-sys", Title: "Fixture Theme"}) + if err := tr.Register("fixture-tpl", func(ctx context.Context, doc map[string]any) templates.HTMLComponent { + title, _ := doc["title"].(string) + return textComponent("" + title + "") + }); err != nil { + return err + } + return tr.RegisterPageTemplate("fixture-sys", templates.PageTemplateMeta{ + Key: "fixture-page", Title: "Fixture Page", Slots: []string{"main"}, + }, func(ctx context.Context, doc map[string]any) templates.HTMLComponent { + return textComponent("") + }) + }, + AdminPages: func() []plugin.AdminPage { + return []plugin.AdminPage{{Key: "fixture", Title: "Fixture Admin", Icon: "puzzle", Route: "/admin/fixture"}} + }, + AIActions: func() []plugin.AIAction { + return []plugin.AIAction{{Key: "fixture.summarize", Title: "Summarize", DefaultProvider: "openai", DefaultModel: "gpt-4o"}} + }, + SettingsSchema: func() []byte { return []byte(`{"type":"object"}`) }, + JobHandlers: func(deps plugin.CoreServices) map[string]plugin.JobHandlerFunc { + return map[string]plugin.JobHandlerFunc{ + "fixture_job": func(ctx context.Context, config json.RawMessage, progress func(int, int, string)) (json.RawMessage, error) { + return json.RawMessage(`{"ok":true}`), nil + }, + } + }, + Load: func(deps plugin.CoreServices) error { return nil }, + Unload: func(ctx context.Context) error { return nil }, + RequiredIconPacks: []string{"tabler"}, + } +} + +// --- helpers --- + +func invokeHook(t *testing.T, g *guest, hook abiv1.Hook, payload proto.Message) *abiv1.InvokeResponse { + t.Helper() + var payloadBytes []byte + if payload != nil { + b, err := proto.Marshal(payload) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + payloadBytes = b + } + req, err := proto.Marshal(&abiv1.InvokeRequest{Hook: hook, Payload: payloadBytes}) + if err != nil { + t.Fatalf("marshal envelope: %v", err) + } + out := g.invoke(uint32(hook), req) + resp := &abiv1.InvokeResponse{} + if err := proto.Unmarshal(out, resp); err != nil { + t.Fatalf("unmarshal InvokeResponse: %v", err) + } + return resp +} + +// --- tests --- + +func TestServeInstallsRuntime(t *testing.T) { + t.Cleanup(func() { current = nil }) + Serve(fixtureRegistration()) + if current == nil { + t.Fatal("Serve did not install the guest runtime") + } + resp := invokeHook(t, current, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1}) + if resp.GetError() != nil { + t.Fatalf("describe via Serve-installed runtime failed: %v", resp.GetError()) + } +} + +func TestDescribeManifest(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1}) + if resp.GetError() != nil { + t.Fatalf("describe returned error: %v", resp.GetError()) + } + dr := &abiv1.DescribeResponse{} + if err := proto.Unmarshal(resp.GetPayload(), dr); err != nil { + t.Fatalf("unmarshal DescribeResponse: %v", err) + } + m := dr.GetManifest() + if m == nil { + t.Fatal("nil manifest") + } + if m.GetAbiVersion() != 1 { + t.Errorf("abi_version = %d, want 1", m.GetAbiVersion()) + } + if m.GetName() != "fixture" || m.GetVersion() != "0.1.0" { + t.Errorf("name/version = %q/%q", m.GetName(), m.GetVersion()) + } + if len(m.GetDependencies()) != 1 || m.GetDependencies()[0].GetPlugin() != "other" { + t.Errorf("dependencies = %v", m.GetDependencies()) + } + if len(m.GetBlocks()) != 1 || m.GetBlocks()[0].GetKey() != "fixture:hello" { + t.Fatalf("blocks = %v, want one block fixture:hello", m.GetBlocks()) + } + if m.GetBlocks()[0].GetCategory() != "content" { + t.Errorf("block category = %q", m.GetBlocks()[0].GetCategory()) + } + if len(m.GetBlockTemplateOverrides()) != 1 || + m.GetBlockTemplateOverrides()[0].GetTemplateKey() != "landing" || + m.GetBlockTemplateOverrides()[0].GetBlockKey() != "hello" || + m.GetBlockTemplateOverrides()[0].GetSource() != "fixture" { + t.Errorf("overrides = %v", m.GetBlockTemplateOverrides()) + } + if len(m.GetTemplateKeys()) != 1 || m.GetTemplateKeys()[0] != "fixture-tpl" { + t.Errorf("template_keys = %v", m.GetTemplateKeys()) + } + if len(m.GetSystemTemplates()) != 1 || m.GetSystemTemplates()[0].GetKey() != "fixture-sys" { + t.Errorf("system_templates = %v", m.GetSystemTemplates()) + } + if len(m.GetPageTemplates()) != 1 || m.GetPageTemplates()[0].GetSystemKey() != "fixture-sys" || + m.GetPageTemplates()[0].GetKey() != "fixture-page" { + t.Errorf("page_templates = %v", m.GetPageTemplates()) + } + if len(m.GetAdminPages()) != 1 || m.GetAdminPages()[0].GetRoute() != "/admin/fixture" { + t.Errorf("admin_pages = %v", m.GetAdminPages()) + } + if len(m.GetAiActions()) != 1 || m.GetAiActions()[0].GetKey() != "fixture.summarize" { + t.Errorf("ai_actions = %v", m.GetAiActions()) + } + if string(m.GetSettingsSchema()) != `{"type":"object"}` { + t.Errorf("settings_schema = %s", m.GetSettingsSchema()) + } + if len(m.GetJobTypes()) != 1 || m.GetJobTypes()[0] != "fixture_job" { + t.Errorf("job_types = %v", m.GetJobTypes()) + } + if !m.GetHasLoadHook() || !m.GetHasUnloadHook() { + t.Errorf("has_load_hook/has_unload_hook = %v/%v, want true/true", m.GetHasLoadHook(), m.GetHasUnloadHook()) + } + if m.GetHasHttpHandler() || m.GetHasMediaHooks() || m.GetHasProvisioner() { + t.Errorf("unexpected presence flags: http=%v media=%v prov=%v", + m.GetHasHttpHandler(), m.GetHasMediaHooks(), m.GetHasProvisioner()) + } + if len(m.GetRequiredIconPacks()) != 1 || m.GetRequiredIconPacks()[0] != "tabler" { + t.Errorf("required_icon_packs = %v", m.GetRequiredIconPacks()) + } +} + +func TestDescribeRejectsUnknownHostVersion(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 99}) + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED { + t.Fatalf("error = %v, want UNIMPLEMENTED", resp.GetError()) + } +} + +func TestUnknownHook(t *testing.T) { + g := newGuest(fixtureRegistration()) + req, _ := proto.Marshal(&abiv1.InvokeRequest{Hook: abiv1.Hook(99)}) + out := g.invoke(99, req) + resp := &abiv1.InvokeResponse{} + if err := proto.Unmarshal(out, resp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED { + t.Fatalf("error = %v, want UNIMPLEMENTED", resp.GetError()) + } +} + +func TestHookIDEnvelopeMismatch(t *testing.T) { + g := newGuest(fixtureRegistration()) + req, _ := proto.Marshal(&abiv1.InvokeRequest{Hook: abiv1.Hook_HOOK_DESCRIBE}) + out := g.invoke(uint32(abiv1.Hook_HOOK_RENDER_BLOCK), req) + resp := &abiv1.InvokeResponse{} + if err := proto.Unmarshal(out, resp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE { + t.Fatalf("error = %v, want DECODE", resp.GetError()) + } +} + +func TestUndecodableEnvelope(t *testing.T) { + g := newGuest(fixtureRegistration()) + out := g.invoke(uint32(abiv1.Hook_HOOK_DESCRIBE), []byte{0xff, 0xff, 0xff, 0xff}) + resp := &abiv1.InvokeResponse{} + if err := proto.Unmarshal(out, resp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE { + t.Fatalf("error = %v, want DECODE", resp.GetError()) + } +} + +func TestRenderBlock(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "fixture:hello", + ContentJson: []byte(`{"name":"world"}`), + RenderContext: &abiv1.RenderContext{ + TemplateKey: "fixture-tpl", + Page: &abiv1.PageContext{ + Id: "5f0c5f3f-3f1e-4a3a-9b6e-2f4f6a6d7e8f", + Slug: "home", Title: "Home", PostType: "page", Status: "published", + }, + }, + }) + if resp.GetError() != nil { + t.Fatalf("render error: %v", resp.GetError()) + } + rr := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rr); err != nil { + t.Fatalf("unmarshal RenderBlockResponse: %v", err) + } + want := "

hello world on home (tpl fixture-tpl)

" + if rr.GetHtml() != want { + t.Errorf("html = %q, want %q", rr.GetHtml(), want) + } +} + +func TestRenderBlockTemplateOverride(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "hello", + ContentJson: []byte(`{}`), + RenderContext: &abiv1.RenderContext{TemplateKey: "landing"}, + }) + if resp.GetError() != nil { + t.Fatalf("render error: %v", resp.GetError()) + } + rr := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if rr.GetHtml() != "

override hello

" { + t.Errorf("html = %q", rr.GetHtml()) + } +} + +func TestRenderBlockUnknownKey(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "nope", ContentJson: []byte(`{}`), + }) + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED { + t.Fatalf("error = %v, want UNIMPLEMENTED", resp.GetError()) + } +} + +func TestPanickingBlockReturnsAbiErrorAndStaysCallable(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "fixture:hello", ContentJson: []byte(`{"boom":true}`), + }) + e := resp.GetError() + if e.GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL { + t.Fatalf("error = %v, want INTERNAL", e) + } + if !strings.Contains(e.GetMessage(), "kaboom") { + t.Errorf("error message %q does not mention panic value", e.GetMessage()) + } + // The guest must remain callable after a recovered panic. + resp2 := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "fixture:hello", ContentJson: []byte(`{"name":"again"}`), + }) + if resp2.GetError() != nil { + t.Fatalf("second render failed: %v", resp2.GetError()) + } + rr := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp2.GetPayload(), rr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !strings.Contains(rr.GetHtml(), "again") { + t.Errorf("html = %q", rr.GetHtml()) + } +} + +func TestRenderTemplate(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TEMPLATE, &abiv1.RenderTemplateRequest{ + TemplateKey: "fixture-tpl", + DocJson: []byte(`{"title":"My Page"}`), + }) + if resp.GetError() != nil { + t.Fatalf("render error: %v", resp.GetError()) + } + rr := &abiv1.RenderTemplateResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if string(rr.GetHtml()) != "My Page" { + t.Errorf("html = %q", rr.GetHtml()) + } +} + +func TestJobHook(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_JOB, &abiv1.JobRequest{ + JobType: "fixture_job", ConfigJson: []byte(`{}`), + }) + if resp.GetError() != nil { + t.Fatalf("job error: %v", resp.GetError()) + } + jr := &abiv1.JobResponse{} + if err := proto.Unmarshal(resp.GetPayload(), jr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if string(jr.GetResultJson()) != `{"ok":true}` { + t.Errorf("result = %s", jr.GetResultJson()) + } +} + +func TestLoadUnloadHooks(t *testing.T) { + g := newGuest(fixtureRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_LOAD, &abiv1.LoadRequest{ + HostConfig: &abiv1.HostConfig{AppUrl: "https://example.test", MediaPath: "/media"}, + }) + if resp.GetError() != nil { + t.Fatalf("load error: %v", resp.GetError()) + } + resp = invokeHook(t, g, abiv1.Hook_HOOK_UNLOAD, &abiv1.UnloadRequest{}) + if resp.GetError() != nil { + t.Fatalf("unload error: %v", resp.GetError()) + } +} diff --git a/plugin/wasmguest/doc.go b/plugin/wasmguest/doc.go new file mode 100644 index 0000000..5c75c69 --- /dev/null +++ b/plugin/wasmguest/doc.go @@ -0,0 +1,46 @@ +// Package wasmguest is the guest half of the BlockNinja wasm plugin ABI: +// the go:wasmexport entry points (bn_alloc / bn_invoke / bn_free), guest +// memory pinning, the generic host-call import, and the dispatch table that +// adapts an unmodified plugin.PluginRegistration to ABI hook calls. +// +// The wire contract lives in core/docs/wasm-abi.md; the messages in +// git.dev.alexdunmow.com/block/pluginsdk/abi/v1. +// +// # Plugin boilerplate +// +// A plugin becomes a wasm plugin by adding one main file next to its +// Registration: +// +// //go:build wasip1 +// +// package main +// +// import "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest" +// +// func init() { wasmguest.Serve(Registration) } +// func main() {} // never called — reactor mode +// +// and compiling in REACTOR mode (Go >= 1.24 toolchain; this repo's floor is +// higher): +// +// GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm . +// +// Reactor modules export `_initialize` instead of `_start`: the host runs +// `_initialize` once per instance — which runs the init func above, hence +// Serve — before any bn_invoke. Serve is non-blocking: it stores the +// registration, runs the Register pass against capture registries, and +// returns; all work then arrives through bn_invoke. +// +// Command mode (plain `go build`) does NOT work and must not be used: its +// `_start` runs main synchronously, so a blocking main deadlocks the Go +// runtime ("all goroutines are asleep") and a returning main exits and +// closes the module — either way the exports are never callable +// (empirically verified against wazero v1.12.0; see wasm-abi.md). +// +// # Build shape +// +// Dispatch, describe, capture-registry, and context-reconstruction logic is +// plain Go and builds on every GOOS so it stays natively testable. Only +// exports.go (go:wasmexport, memory pinning) and hostcalls.go +// (go:wasmimport blockninja host_call) carry the wasip1 build tag. +package wasmguest diff --git a/plugin/wasmguest/exports.go b/plugin/wasmguest/exports.go new file mode 100644 index 0000000..baf006e --- /dev/null +++ b/plugin/wasmguest/exports.go @@ -0,0 +1,98 @@ +//go:build wasip1 + +package wasmguest + +import ( + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "unsafe" +) + +// pinned keeps host-visible buffers reachable so the Go GC never frees (or, +// under a future moving GC, relocates) memory the host holds a raw pointer +// into. Keyed by the buffer's linear-memory address. Wasm instances are +// single-threaded per the ABI (one bn_invoke at a time), so no locking. +var pinned = make(map[uint32][]byte) + +// lastResponse pins the most recent bn_invoke response buffer; per the ABI +// it stays valid until the next bn_invoke on this instance. +var lastResponse []byte + +// bn_alloc allocates size bytes of guest memory for the host to write into. +// The region stays valid until the current bn_invoke call returns (request +// buffers) or until released with bn_free / consumed by the shim (host-call +// responses). +// +//go:wasmexport bn_alloc +func bnAlloc(size uint32) uint32 { + if size == 0 { + return 0 + } + buf := make([]byte, size) + ptr := bufPtr(buf) + pinned[ptr] = buf + return ptr +} + +// bn_free releases a buffer previously returned by bn_alloc. Unknown +// pointers are ignored (the shim may have already released the buffer). +// +//go:wasmexport bn_free +func bnFree(ptr uint32) { + delete(pinned, ptr) +} + +// bn_invoke dispatches one hook call. (ptr, len) frames a serialized +// InvokeRequest written by the host into bn_alloc'd memory; the packed +// return value ((ptr << 32) | len) frames a serialized InvokeResponse valid +// until the next bn_invoke. A return of 0 means the guest could not even +// produce an error envelope; the host treats it as INTERNAL and discards +// the instance. +// +//go:wasmexport bn_invoke +func bnInvoke(hookID uint32, ptr uint32, length uint32) uint64 { + var ( + req []byte + known bool + ) + if buf, ok := pinned[ptr]; ok && uint32(len(buf)) >= length { + req = buf[:length:length] + known = true + } + + var resp []byte + switch { + case !known: + // The ABI requires request buffers to come from bn_alloc; anything + // else is a protocol violation, not readable guest memory. + resp = errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE, + "bn_invoke request pointer was not allocated via bn_alloc") + case current == nil: + resp = errorResponse(abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, + "wasmguest.Serve was never called — plugin main package is missing the init boilerplate (see wasmguest doc.go)") + default: + resp = current.invoke(hookID, req) + } + + // Request buffer ownership ends with this call (ABI: valid until + // bn_invoke returns). Release it whether or not the host calls bn_free. + delete(pinned, ptr) + + // A logically-empty InvokeResponse (Error nil + empty payload — e.g. + // LoadResponse/UnloadResponse) proto-marshals to ZERO bytes. That is a + // valid success envelope, not the "couldn't produce an envelope" sentinel, + // so frame it as (ptr, 0) with a real pointer. Returning packed 0 here + // would make the host mistake every empty-response hook (notably a + // successful LOAD) for an INTERNAL failure and discard the instance. A + // 1-byte backing keeps the pointer valid while the reported length stays 0 + // (the host reads zero bytes → an empty InvokeResponse, which is correct). + if len(resp) == 0 { + lastResponse = []byte{0} + return uint64(bufPtr(lastResponse)) << 32 + } + lastResponse = resp + return uint64(bufPtr(resp))<<32 | uint64(uint32(len(resp))) +} + +func bufPtr(b []byte) uint32 { + return uint32(uintptr(unsafe.Pointer(&b[0]))) +} diff --git a/plugin/wasmguest/hostcalls.go b/plugin/wasmguest/hostcalls.go new file mode 100644 index 0000000..df0fbd5 --- /dev/null +++ b/plugin/wasmguest/hostcalls.go @@ -0,0 +1,125 @@ +//go:build wasip1 + +package wasmguest + +import ( + "fmt" + "runtime" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm" + "google.golang.org/protobuf/proto" +) + +// Guest→host capability calls use ONE generic import rather than one import +// per capability family: the method string inside HostCallRequest already +// selects the family (".", including the db.* driver +// messages), so per-family import symbols would add ~40 declarations on +// both sides of the boundary for zero type safety — the payloads are opaque +// protobuf bytes either way. Decision recorded in core/docs/wasm-abi.md. +// +// Shape (mirrors bn_invoke in reverse): the guest passes (ptr, len) framing +// a serialized HostCallRequest; the host returns packed ((ptr << 32) | len) +// framing a HostCallResponse it wrote into guest memory via bn_alloc. +// packed == 0 means the host could not produce an envelope. +// +//go:wasmimport blockninja host_call +//go:noescape +func hostCall(ptr uint32, size uint32) uint64 + +// init binds the capability-stub transport (package caps) to the real +// host_call-backed CallHost. Only wasip1 has a host to call; native builds +// leave caps' transport nil, so their capability calls fail cleanly. The same +// transport backs the "bnwasm" database/sql driver for plugins that open a +// *sql.DB (the pgx-flavored plugin.Pool path binds capTransport in dispatch). +func init() { + capTransport = CallHost + bnwasm.SetDefaultTransport(bnwasm.Transport(CallHost)) +} + +// HostError is a failed capability call's AbiError surfaced as a Go error. +type HostError struct { + Code abiv1.AbiErrorCode + Message string +} + +func (e *HostError) Error() string { + return fmt.Sprintf("host call failed (%s): %s", e.Code, e.Message) +} + +// AbiErrorCode exposes the underlying ABI error code so the capability stubs +// (package caps) can map a DEADLINE_EXCEEDED reply onto context.DeadlineExceeded +// without importing this wasip1-only package. +func (e *HostError) AbiErrorCode() abiv1.AbiErrorCode { return e.Code } + +// HostCall performs one guest→host capability call. method is +// "." (e.g. "crypto.encrypt_secret"); payload is the +// serialized family request message. It returns the serialized family +// response payload. The capability-stub implementations of the CoreServices +// interfaces are built on this. +func HostCall(method string, payload []byte) ([]byte, error) { + raw, err := proto.Marshal(&abiv1.HostCallRequest{Method: method, Payload: payload}) + if err != nil { + return nil, fmt.Errorf("wasmguest: marshal HostCallRequest: %w", err) + } + var ptr, size uint32 + if len(raw) > 0 { + ptr = bufPtr(raw) + size = uint32(len(raw)) + } + packed := hostCall(ptr, size) + runtime.KeepAlive(raw) + if packed == 0 { + return nil, &HostError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, + Message: "host returned packed 0 (no envelope) for " + method, + } + } + + respPtr := uint32(packed >> 32) + respLen := uint32(packed) + buf, ok := pinned[respPtr] + if !ok || uint32(len(buf)) < respLen { + // The ABI requires the host to write responses into bn_alloc'd + // memory; anything else is a protocol violation. + return nil, &HostError{ + Code: abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE, + Message: "host response pointer was not allocated via bn_alloc for " + method, + } + } + respBytes := buf[:respLen:respLen] + + resp := &abiv1.HostCallResponse{} + unmarshalErr := proto.Unmarshal(respBytes, resp) // copies; safe to release the buffer + delete(pinned, respPtr) // host allocated via bn_alloc; the shim owns the release + if unmarshalErr != nil { + return nil, fmt.Errorf("wasmguest: decode HostCallResponse for %s: %w", method, unmarshalErr) + } + if e := resp.GetError(); e != nil { + return nil, &HostError{Code: e.GetCode(), Message: e.GetMessage()} + } + return resp.GetPayload(), nil +} + +// CallHost marshals req, performs the capability call, and unmarshals the +// response into resp — the typed convenience wrapper capability stubs use. +func CallHost(method string, req, resp proto.Message) error { + var payload []byte + if req != nil { + b, err := proto.Marshal(req) + if err != nil { + return fmt.Errorf("wasmguest: marshal %s request: %w", method, err) + } + payload = b + } + out, err := HostCall(method, payload) + if err != nil { + return err + } + if resp != nil { + if err := proto.Unmarshal(out, resp); err != nil { + return fmt.Errorf("wasmguest: decode %s response: %w", method, err) + } + } + return nil +} diff --git a/plugin/wasmguest/registries.go b/plugin/wasmguest/registries.go new file mode 100644 index 0000000..a8311af --- /dev/null +++ b/plugin/wasmguest/registries.go @@ -0,0 +1,114 @@ +package wasmguest + +import ( + "io/fs" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/templates" +) + +// captureBlockRegistry implements blocks.BlockRegistry by recording every +// registration instead of wiring it into a live host. It backs both DESCRIBE +// (metadata enumeration) and runtime dispatch (BlockFunc lookup). +// +// It is always used behind plugin.NewPluginBlockRegistry, so keys arrive +// already plugin-prefixed and overrides arrive with their source set — +// identical to what a plugin sees in the .so world. +type captureBlockRegistry struct { + metas []blocks.BlockMeta + funcs map[string]blocks.BlockFunc // block key → fn + overrides []blockOverride +} + +type blockOverride struct { + templateKey string + blockKey string + source string + fn blocks.BlockFunc +} + +func newCaptureBlockRegistry() *captureBlockRegistry { + return &captureBlockRegistry{funcs: make(map[string]blocks.BlockFunc)} +} + +func (r *captureBlockRegistry) Register(meta blocks.BlockMeta, fn blocks.BlockFunc) { + r.metas = append(r.metas, meta) + r.funcs[meta.Key] = fn +} + +func (r *captureBlockRegistry) RegisterTemplateOverride(templateKey, blockKey string, fn blocks.BlockFunc) { + r.overrides = append(r.overrides, blockOverride{templateKey: templateKey, blockKey: blockKey, fn: fn}) +} + +func (r *captureBlockRegistry) RegisterTemplateOverrideWithSource(templateKey, blockKey, source string, fn blocks.BlockFunc) { + r.overrides = append(r.overrides, blockOverride{templateKey: templateKey, blockKey: blockKey, source: source, fn: fn}) +} + +// LoadSchemasFromFS is a no-op in the wasm guest: block schemas ship in the +// .bnp artifact's schemas/ directory and are loaded host-side. +func (r *captureBlockRegistry) LoadSchemasFromFS(fs.FS) error { return nil } + +func (r *captureBlockRegistry) LoadSchemasFromFSWithPrefix(fs.FS, string) error { return nil } + +// lookup resolves a block function for dispatch: a template-scoped override +// wins over the base registration, mirroring the host registry's behavior. +func (r *captureBlockRegistry) lookup(blockKey, templateKey string) (blocks.BlockFunc, bool) { + if templateKey != "" { + for _, o := range r.overrides { + if o.templateKey == templateKey && o.blockKey == blockKey { + return o.fn, true + } + } + } + fn, ok := r.funcs[blockKey] + return fn, ok +} + +// captureTemplateRegistry implements templates.TemplateRegistry by recording +// registrations for DESCRIBE and runtime dispatch. +type captureTemplateRegistry struct { + templateKeys []string // insertion order of Register calls + funcs map[string]templates.TemplateFunc + systemTemplates []templates.SystemTemplateMeta + pageTemplates []capturedPageTemplate + emailWrappers []capturedEmailWrapper +} + +type capturedPageTemplate struct { + systemKey string + meta templates.PageTemplateMeta +} + +type capturedEmailWrapper struct { + systemKey string + fn templates.EmailWrapperFunc +} + +func newCaptureTemplateRegistry() *captureTemplateRegistry { + return &captureTemplateRegistry{funcs: make(map[string]templates.TemplateFunc)} +} + +func (r *captureTemplateRegistry) Register(key string, fn templates.TemplateFunc) error { + r.templateKeys = append(r.templateKeys, key) + r.funcs[key] = fn + return nil +} + +func (r *captureTemplateRegistry) RegisterSystemTemplate(meta templates.SystemTemplateMeta) { + r.systemTemplates = append(r.systemTemplates, meta) +} + +func (r *captureTemplateRegistry) RegisterPageTemplate(systemKey string, meta templates.PageTemplateMeta, fn templates.TemplateFunc) error { + r.pageTemplates = append(r.pageTemplates, capturedPageTemplate{systemKey: systemKey, meta: meta}) + r.funcs[meta.Key] = fn + return nil +} + +func (r *captureTemplateRegistry) RegisterEmailWrapper(systemKey string, fn templates.EmailWrapperFunc) { + r.emailWrappers = append(r.emailWrappers, capturedEmailWrapper{systemKey: systemKey, fn: fn}) +} + +func (r *captureTemplateRegistry) lookup(key string) (templates.TemplateFunc, bool) { + fn, ok := r.funcs[key] + return fn, ok +} diff --git a/plugin/wasmguest/render_capability_test.go b/plugin/wasmguest/render_capability_test.go new file mode 100644 index 0000000..e8e28af --- /dev/null +++ b/plugin/wasmguest/render_capability_test.go @@ -0,0 +1,220 @@ +package wasmguest + +import ( + "context" + "errors" + "strings" + "testing" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "git.dev.alexdunmow.com/block/pluginsdk/templates" + "google.golang.org/protobuf/proto" +) + +// tagFilterRegistration registers a powered block plus a custom tag and +// filter, exercising the render-as-a-host-capability surface (RENDER_TAG, +// APPLY_FILTER, powered RENDER_BLOCK, and DESCRIBE declared_tags/filters). +func tagFilterRegistration() plugin.PluginRegistration { + return plugin.PluginRegistration{ + Name: "capfx", + Version: "0.1.0", + Register: func(_ templates.TemplateRegistry, br blocks.BlockRegistry) error { + br.Register(blocks.BlockMeta{Key: "hero", Title: "Hero", Category: blocks.CategoryContent}, + func(ctx context.Context, content map[string]any) string { + title, _ := content["title"].(string) + // Powered: return template + data; the host renders it. + return blocks.PoweredBlock("

{{ title }}

", map[string]any{"title": title}) + }) + blocks.RegisterTag("greet", func(args map[string]any, rctx blocks.RenderContext) (string, error) { + who, _ := args["who"].(string) + if who == "boom" { + panic("tag kaboom") + } + if who == "err" { + return "", errors.New("greet failed") + } + // Reads render context via the standard accessors. + tpl := blocks.GetTemplateKey(rctx) + return "hi " + who + " on " + tpl, nil + }) + blocks.RegisterFilter("upper", func(input string, args map[string]any) (string, error) { + if input == "err" { + return "", errors.New("upper failed") + } + return strings.ToUpper(input), nil + }) + return nil + }, + } +} + +func TestDescribeEmitsDeclaredTagsAndFilters(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1}) + if resp.GetError() != nil { + t.Fatalf("describe error: %v", resp.GetError()) + } + dr := &abiv1.DescribeResponse{} + if err := proto.Unmarshal(resp.GetPayload(), dr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + m := dr.GetManifest() + if got := m.GetDeclaredTags(); len(got) != 1 || got[0] != "greet" { + t.Errorf("declared_tags = %v, want [greet]", got) + } + if got := m.GetDeclaredFilters(); len(got) != 1 || got[0] != "upper" { + t.Errorf("declared_filters = %v, want [upper]", got) + } +} + +// TestDescribeTagRegistryIsPerGuest proves runRegister's reset isolates the +// package-level tag/filter registry per installed registration: a tag-free +// registration must describe zero tags even after a prior tag-registering one. +func TestDescribeTagRegistryIsPerGuest(t *testing.T) { + _ = newGuest(tagFilterRegistration()) // populates the registry + g := newGuest(fixtureRegistration()) // fixtureRegistration registers no tags + resp := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1}) + dr := &abiv1.DescribeResponse{} + if err := proto.Unmarshal(resp.GetPayload(), dr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got := dr.GetManifest().GetDeclaredTags(); len(got) != 0 { + t.Errorf("declared_tags = %v, want empty (registry not reset per guest)", got) + } +} + +func TestPoweredBlockRoundTrip(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "capfx:hero", + ContentJson: []byte(`{"title":"Welcome"}`), + }) + if resp.GetError() != nil { + t.Fatalf("render error: %v", resp.GetError()) + } + rb := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rb); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if rb.GetHtml() != "" { + t.Errorf("html = %q, want empty (powered)", rb.GetHtml()) + } + p := rb.GetPowered() + if p == nil { + t.Fatal("nil Powered result") + } + if p.GetTemplate() != "

{{ title }}

" { + t.Errorf("template = %q", p.GetTemplate()) + } + if !strings.Contains(string(p.GetDataJson()), `"title":"Welcome"`) { + t.Errorf("data_json = %s", p.GetDataJson()) + } +} + +func TestRenderTagRoundTrip(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TAG, &abiv1.RenderTagRequest{ + TagName: "greet", + ArgsJson: []byte(`{"who":"sam"}`), + RenderContext: &abiv1.RenderContext{TemplateKey: "landing"}, + }) + if resp.GetError() != nil { + t.Fatalf("render tag error: %v", resp.GetError()) + } + tr := &abiv1.RenderTagResponse{} + if err := proto.Unmarshal(resp.GetPayload(), tr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if tr.GetHtml() != "hi sam on landing" || tr.GetError() != "" { + t.Errorf("html=%q error=%q", tr.GetHtml(), tr.GetError()) + } +} + +func TestRenderTagFnErrorSurfacesInResponse(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TAG, &abiv1.RenderTagRequest{ + TagName: "greet", ArgsJson: []byte(`{"who":"err"}`), + }) + if resp.GetError() != nil { + t.Fatalf("unexpected AbiError: %v", resp.GetError()) + } + tr := &abiv1.RenderTagResponse{} + if err := proto.Unmarshal(resp.GetPayload(), tr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if tr.GetHtml() != "" || !strings.Contains(tr.GetError(), "greet failed") { + t.Errorf("html=%q error=%q, want error carrying 'greet failed'", tr.GetHtml(), tr.GetError()) + } +} + +func TestRenderTagUnknownIsUnimplemented(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TAG, &abiv1.RenderTagRequest{TagName: "nope"}) + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED { + t.Fatalf("error = %v, want UNIMPLEMENTED", resp.GetError()) + } +} + +func TestRenderTagPanicRecoversAndStaysCallable(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TAG, &abiv1.RenderTagRequest{ + TagName: "greet", ArgsJson: []byte(`{"who":"boom"}`), + }) + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL { + t.Fatalf("error = %v, want INTERNAL", resp.GetError()) + } + if !strings.Contains(resp.GetError().GetMessage(), "tag kaboom") { + t.Errorf("message = %q, want panic value", resp.GetError().GetMessage()) + } + // Guest stays callable after a recovered tag panic. + resp2 := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TAG, &abiv1.RenderTagRequest{ + TagName: "greet", ArgsJson: []byte(`{"who":"again"}`), + }) + if resp2.GetError() != nil { + t.Fatalf("post-panic tag failed: %v", resp2.GetError()) + } +} + +func TestApplyFilterRoundTrip(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_APPLY_FILTER, &abiv1.ApplyFilterRequest{ + FilterName: "upper", Input: "hello", + }) + if resp.GetError() != nil { + t.Fatalf("apply filter error: %v", resp.GetError()) + } + fr := &abiv1.ApplyFilterResponse{} + if err := proto.Unmarshal(resp.GetPayload(), fr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if fr.GetOutput() != "HELLO" || fr.GetError() != "" { + t.Errorf("output=%q error=%q", fr.GetOutput(), fr.GetError()) + } +} + +func TestApplyFilterFnErrorSurfacesInResponse(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_APPLY_FILTER, &abiv1.ApplyFilterRequest{ + FilterName: "upper", Input: "err", + }) + if resp.GetError() != nil { + t.Fatalf("unexpected AbiError: %v", resp.GetError()) + } + fr := &abiv1.ApplyFilterResponse{} + if err := proto.Unmarshal(resp.GetPayload(), fr); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if fr.GetOutput() != "" || !strings.Contains(fr.GetError(), "upper failed") { + t.Errorf("output=%q error=%q", fr.GetOutput(), fr.GetError()) + } +} + +func TestApplyFilterUnknownIsUnimplemented(t *testing.T) { + g := newGuest(tagFilterRegistration()) + resp := invokeHook(t, g, abiv1.Hook_HOOK_APPLY_FILTER, &abiv1.ApplyFilterRequest{FilterName: "nope"}) + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED { + t.Fatalf("error = %v, want UNIMPLEMENTED", resp.GetError()) + } +} diff --git a/plugin/wasmguest/testdata/capfixture/main.go b/plugin/wasmguest/testdata/capfixture/main.go new file mode 100644 index 0000000..c52f821 --- /dev/null +++ b/plugin/wasmguest/testdata/capfixture/main.go @@ -0,0 +1,8 @@ +//go:build wasip1 + +package main + +import "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest" + +func init() { wasmguest.Serve(Registration) } +func main() {} // never called — reactor mode diff --git a/plugin/wasmguest/testdata/capfixture/plugin.mod b/plugin/wasmguest/testdata/capfixture/plugin.mod new file mode 100644 index 0000000..2bb3a67 --- /dev/null +++ b/plugin/wasmguest/testdata/capfixture/plugin.mod @@ -0,0 +1,6 @@ +[plugin] +name = "capfixture" +scope = "@blockninja" +version = "0.0.1" +description = "Negative fixture: Register hits a host capability at describe time" +kind = "plugin" diff --git a/plugin/wasmguest/testdata/capfixture/registration.go b/plugin/wasmguest/testdata/capfixture/registration.go new file mode 100644 index 0000000..4e05521 --- /dev/null +++ b/plugin/wasmguest/testdata/capfixture/registration.go @@ -0,0 +1,38 @@ +// Package main is a NEGATIVE build fixture: its Register hook reaches a host +// capability (a db.* call) at describe time, which is illegal — DESCRIBE runs +// with no live host. `ninja plugin build` must surface an actionable error +// naming the offending call rather than hanging or crashing. +package main + +import ( + "context" + "database/sql" + "fmt" + + // Registers the "bnwasm" database/sql driver (its init runs sql.Register). + _ "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "git.dev.alexdunmow.com/block/pluginsdk/templates" +) + +var Registration = plugin.PluginRegistration{ + Name: "capfixture", + Version: "0.0.1", + Register: func(_ templates.TemplateRegistry, _ blocks.BlockRegistry) error { + // Illegal describe-time capability use: query the host DB from + // Register. Over the packer's failing host stub this returns an error + // naming "db.query". + db, err := sql.Open("bnwasm", "") + if err != nil { + return err + } + defer func() { _ = db.Close() }() + var n int + if err := db.QueryRowContext(context.Background(), "SELECT 1").Scan(&n); err != nil { + return fmt.Errorf("describe-time db probe: %w", err) + } + return nil + }, +} diff --git a/plugin/wasmguest/testdata/fixture/main.go b/plugin/wasmguest/testdata/fixture/main.go new file mode 100644 index 0000000..c52f821 --- /dev/null +++ b/plugin/wasmguest/testdata/fixture/main.go @@ -0,0 +1,8 @@ +//go:build wasip1 + +package main + +import "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest" + +func init() { wasmguest.Serve(Registration) } +func main() {} // never called — reactor mode diff --git a/plugin/wasmguest/testdata/fixture/plugin.mod b/plugin/wasmguest/testdata/fixture/plugin.mod new file mode 100644 index 0000000..4c8f7cd --- /dev/null +++ b/plugin/wasmguest/testdata/fixture/plugin.mod @@ -0,0 +1,8 @@ +[plugin] +name = "wasmfixture" +display_name = "Wasm Fixture" +scope = "@blockninja" +version = "0.0.1" +description = "WO-WZ-002 acceptance fixture: one block, one template, one admin page" +kind = "plugin" +data_dir = true diff --git a/plugin/wasmguest/testdata/fixture/registration.go b/plugin/wasmguest/testdata/fixture/registration.go new file mode 100644 index 0000000..6339c31 --- /dev/null +++ b/plugin/wasmguest/testdata/fixture/registration.go @@ -0,0 +1,97 @@ +// Package main is the WO-WZ-002 acceptance fixture: a minimal plugin with +// one block, one template, and one admin page, compiled to wasm with only +// the wasmguest boilerplate (main.go). +package main + +import ( + "context" + "fmt" + "io" + "strings" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/plugin" + "git.dev.alexdunmow.com/block/pluginsdk/templates" +) + +// capReport is populated by the Load hook from capability calls, then +// surfaced through the greeting block ({"report": true}). It is the runtime +// proof that deps.Content / deps.Settings / deps.Bridge cross the wasm ABI +// end-to-end through the guest capability stubs (WO-WZ-003 acceptance). +var capReport string + +// Registration is the fixture's plugin registration — the same shape a .so +// plugin exports, untouched by the wasm migration. +var Registration = plugin.PluginRegistration{ + Name: "wasmfixture", + Version: "0.0.1", + Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error { + br.Register(blocks.BlockMeta{ + Key: "greeting", + Title: "Greeting", + Description: "Renders a greeting", + Category: blocks.CategoryContent, + }, func(ctx context.Context, content map[string]any) string { + if boom, _ := content["boom"].(bool); boom { + panic("fixture kaboom") + } + if report, _ := content["report"].(bool); report { + return "

capreport:" + capReport + "

" + } + name, _ := content["name"].(string) + if name == "" { + name = "world" + } + if powered, _ := content["powered"].(bool); powered { + // Powered path: hand the host a template + data instead of final + // HTML, so the host renders it (pongo2) after this call returns. + return blocks.PoweredBlock("

hello {{ name }}

", map[string]any{"name": name}) + } + return fmt.Sprintf("

hello %s

", name) + }) + // Custom template tag + filter the host wires from manifest.declared_tags + // / declared_filters, invoked via HOOK_RENDER_TAG / HOOK_APPLY_FILTER. + blocks.RegisterTag("shout", func(args map[string]any, rctx blocks.RenderContext) (string, error) { + msg, _ := args["msg"].(string) + return "" + strings.ToUpper(msg) + "", nil + }) + blocks.RegisterFilter("exclaim", func(input string, args map[string]any) (string, error) { + return input + "!", nil + }) + return tr.Register("wasmfixture-page", func(ctx context.Context, doc map[string]any) templates.HTMLComponent { + title, _ := doc["title"].(string) + return htmlString("" + title + "") + }) + }, + // Load exercises the guest capability stubs exactly as a real plugin's + // service code does — unchanged calls against deps.Content / deps.Settings + // / deps.Bridge — proving they compile and cross the ABI for wasip1. + Load: func(deps plugin.CoreServices) error { + page, err := deps.Content.GetPage(context.Background(), "about") + if err != nil { + capReport = "content-err:" + err.Error() + return nil + } + site, err := deps.Settings.GetSiteSettings(context.Background()) + if err != nil { + capReport = "settings-err:" + err.Error() + return nil + } + svc := deps.Bridge.GetService("other", "search") // cannot cross; expected nil + capReport = fmt.Sprintf("page=%s site=%v bridge_nil=%t", + page.Title, site["site_name"], svc == nil) + return nil + }, + AdminPages: func() []plugin.AdminPage { + return []plugin.AdminPage{{ + Key: "wasmfixture", Title: "Wasm Fixture", Icon: "flask", Route: "/admin/wasmfixture", + }} + }, +} + +type htmlString string + +func (s htmlString) Render(_ context.Context, w io.Writer) error { + _, err := io.WriteString(w, string(s)) + return err +} diff --git a/plugin/wasmguest/wasmhost_test.go b/plugin/wasmguest/wasmhost_test.go new file mode 100644 index 0000000..9df7815 --- /dev/null +++ b/plugin/wasmguest/wasmhost_test.go @@ -0,0 +1,350 @@ +package wasmguest + +// Throwaway wazero host: proves the compiled fixture module honors the ABI +// end to end (WO-WZ-002 acceptance) without the real cms host WO. It builds +// testdata/fixture in reactor mode, instantiates it, and drives bn_alloc / +// bn_invoke / bn_free exactly as core/docs/wasm-abi.md specifies. + +import ( + "context" + "encoding/binary" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + "google.golang.org/protobuf/proto" +) + +// buildFixture compiles testdata/fixture to a reactor-mode wasm module. +func buildFixture(t *testing.T) string { + t.Helper() + out := filepath.Join(t.TempDir(), "fixture.wasm") + cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", out, "./testdata/fixture") + cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm") + if b, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("build fixture: %v\n%s", err, b) + } + info, err := os.Stat(out) + if err != nil { + t.Fatalf("stat fixture: %v", err) + } + t.Logf("fixture.wasm size: %d bytes (%.1f MiB)", info.Size(), float64(info.Size())/(1<<20)) + return out +} + +type wasmInstance struct { + mod api.Module + ctx context.Context +} + +func instantiateFixture(t *testing.T) *wasmInstance { + t.Helper() + wasmPath := buildFixture(t) + ctx := context.Background() + + r := wazero.NewRuntime(ctx) + t.Cleanup(func() { _ = r.Close(ctx) }) + wasi_snapshot_preview1.MustInstantiate(ctx, r) + // The guest now imports blockninja.host_call (capability stubs, WO-WZ-003), + // so the host must export it or instantiation fails. + installBlockninjaHost(t, ctx, r) + + wasmBytes, err := os.ReadFile(wasmPath) + if err != nil { + t.Fatalf("read module: %v", err) + } + // Reactor contract: no _start; run _initialize once per instance before + // any bn_invoke. + mod, err := r.InstantiateWithConfig(ctx, wasmBytes, + wazero.NewModuleConfig().WithStartFunctions("_initialize").WithName("fixture")) + if err != nil { + t.Fatalf("instantiate: %v", err) + } + for _, export := range []string{"bn_alloc", "bn_invoke", "bn_free"} { + if mod.ExportedFunction(export) == nil { + t.Fatalf("module does not export %s", export) + } + } + return &wasmInstance{mod: mod, ctx: ctx} +} + +// invoke drives one bn_invoke round trip through guest memory. +func (w *wasmInstance) invoke(t *testing.T, hook abiv1.Hook, payload proto.Message) *abiv1.InvokeResponse { + t.Helper() + payloadBytes, err := proto.Marshal(payload) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + req, err := proto.Marshal(&abiv1.InvokeRequest{Hook: hook, Payload: payloadBytes}) + if err != nil { + t.Fatalf("marshal envelope: %v", err) + } + + alloc := w.mod.ExportedFunction("bn_alloc") + res, err := alloc.Call(w.ctx, uint64(len(req))) + if err != nil { + t.Fatalf("bn_alloc: %v", err) + } + ptr := uint32(res[0]) + if ptr == 0 { + t.Fatal("bn_alloc returned 0") + } + if !w.mod.Memory().Write(ptr, req) { + t.Fatalf("write request at %d (len %d) out of range", ptr, len(req)) + } + + res, err = w.mod.ExportedFunction("bn_invoke").Call(w.ctx, uint64(hook), uint64(ptr), uint64(len(req))) + if err != nil { + t.Fatalf("bn_invoke: %v", err) + } + packed := res[0] + if packed == 0 { + t.Fatal("bn_invoke returned packed 0 (guest could not produce an envelope)") + } + respPtr := uint32(packed >> 32) + respLen := uint32(packed) + respBytes, ok := w.mod.Memory().Read(respPtr, respLen) + if !ok { + t.Fatalf("read response at %d (len %d) out of range", respPtr, respLen) + } + resp := &abiv1.InvokeResponse{} + if err := proto.Unmarshal(respBytes, resp); err != nil { + t.Fatalf("unmarshal InvokeResponse: %v", err) + } + // Host-driven release of the request buffer (guest also self-releases; + // bn_free must tolerate both). + if _, err := w.mod.ExportedFunction("bn_free").Call(w.ctx, uint64(ptr)); err != nil { + t.Fatalf("bn_free: %v", err) + } + return resp +} + +func TestWasmFixtureDescribeRoundTrip(t *testing.T) { + if testing.Short() { + t.Skip("compiles a wasm module; skipped in -short") + } + w := instantiateFixture(t) + + resp := w.invoke(t, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1}) + if resp.GetError() != nil { + t.Fatalf("DESCRIBE error: %v", resp.GetError()) + } + dr := &abiv1.DescribeResponse{} + if err := proto.Unmarshal(resp.GetPayload(), dr); err != nil { + t.Fatalf("unmarshal DescribeResponse: %v", err) + } + m := dr.GetManifest() + t.Logf("manifest: name=%q version=%q abi=%d blocks=%d templates=%v admin_pages=%d", + m.GetName(), m.GetVersion(), m.GetAbiVersion(), len(m.GetBlocks()), m.GetTemplateKeys(), len(m.GetAdminPages())) + if m.GetName() != "wasmfixture" || m.GetVersion() != "0.0.1" || m.GetAbiVersion() != 1 { + t.Errorf("manifest identity = %q/%q/abi %d", m.GetName(), m.GetVersion(), m.GetAbiVersion()) + } + if len(m.GetBlocks()) != 1 || m.GetBlocks()[0].GetKey() != "wasmfixture:greeting" { + t.Errorf("blocks = %v", m.GetBlocks()) + } + if len(m.GetTemplateKeys()) != 1 || m.GetTemplateKeys()[0] != "wasmfixture-page" { + t.Errorf("template_keys = %v", m.GetTemplateKeys()) + } + if len(m.GetAdminPages()) != 1 || m.GetAdminPages()[0].GetRoute() != "/admin/wasmfixture" { + t.Errorf("admin_pages = %v", m.GetAdminPages()) + } + + // Same instance renders a block after DESCRIBE. + resp = w.invoke(t, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "wasmfixture:greeting", + ContentJson: []byte(`{"name":"wazero"}`), + }) + if resp.GetError() != nil { + t.Fatalf("RENDER_BLOCK error: %v", resp.GetError()) + } + rb := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rb); err != nil { + t.Fatalf("unmarshal RenderBlockResponse: %v", err) + } + if rb.GetHtml() != "

hello wazero

" { + t.Errorf("html = %q", rb.GetHtml()) + } + + // Template render through the same instance. + resp = w.invoke(t, abiv1.Hook_HOOK_RENDER_TEMPLATE, &abiv1.RenderTemplateRequest{ + TemplateKey: "wasmfixture-page", + DocJson: []byte(`{"title":"Wazero"}`), + }) + if resp.GetError() != nil { + t.Fatalf("RENDER_TEMPLATE error: %v", resp.GetError()) + } + rt := &abiv1.RenderTemplateResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rt); err != nil { + t.Fatalf("unmarshal RenderTemplateResponse: %v", err) + } + if string(rt.GetHtml()) != "Wazero" { + t.Errorf("template html = %q", rt.GetHtml()) + } +} + +// TestWasmFixtureTagFilterPoweredRoundTrip drives the render-as-a-host- +// capability surface through a REAL compiled wasm module: DESCRIBE must +// surface the plugin's declared tags/filters, a powered block must return a +// {template,data} result (not final HTML), and HOOK_RENDER_TAG / +// HOOK_APPLY_FILTER must dispatch to the plugin's registered fns. +func TestWasmFixtureTagFilterPoweredRoundTrip(t *testing.T) { + if testing.Short() { + t.Skip("compiles a wasm module; skipped in -short") + } + w := instantiateFixture(t) + + // DESCRIBE surfaces declared_tags / declared_filters. + resp := w.invoke(t, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1}) + if resp.GetError() != nil { + t.Fatalf("DESCRIBE error: %v", resp.GetError()) + } + dr := &abiv1.DescribeResponse{} + if err := proto.Unmarshal(resp.GetPayload(), dr); err != nil { + t.Fatalf("unmarshal DescribeResponse: %v", err) + } + m := dr.GetManifest() + if got := m.GetDeclaredTags(); len(got) != 1 || got[0] != "shout" { + t.Errorf("declared_tags = %v, want [shout]", got) + } + if got := m.GetDeclaredFilters(); len(got) != 1 || got[0] != "exclaim" { + t.Errorf("declared_filters = %v, want [exclaim]", got) + } + + // A powered block returns {template,data}, not final HTML. + resp = w.invoke(t, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "wasmfixture:greeting", + ContentJson: []byte(`{"name":"wazero","powered":true}`), + }) + if resp.GetError() != nil { + t.Fatalf("powered RENDER_BLOCK error: %v", resp.GetError()) + } + rb := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rb); err != nil { + t.Fatalf("unmarshal RenderBlockResponse: %v", err) + } + if rb.GetHtml() != "" { + t.Errorf("powered block set html = %q, want empty", rb.GetHtml()) + } + p := rb.GetPowered() + if p == nil { + t.Fatal("powered block returned nil Powered result") + } + if p.GetTemplate() != "

hello {{ name }}

" { + t.Errorf("powered template = %q", p.GetTemplate()) + } + if !strings.Contains(string(p.GetDataJson()), `"name":"wazero"`) { + t.Errorf("powered data_json = %s", p.GetDataJson()) + } + + // HOOK_RENDER_TAG dispatches to the registered tag fn (fresh invoke — + // no re-entrancy; RENDER_BLOCK already returned). + resp = w.invoke(t, abiv1.Hook_HOOK_RENDER_TAG, &abiv1.RenderTagRequest{ + TagName: "shout", + ArgsJson: []byte(`{"msg":"hi"}`), + }) + if resp.GetError() != nil { + t.Fatalf("RENDER_TAG error: %v", resp.GetError()) + } + tr := &abiv1.RenderTagResponse{} + if err := proto.Unmarshal(resp.GetPayload(), tr); err != nil { + t.Fatalf("unmarshal RenderTagResponse: %v", err) + } + if tr.GetHtml() != "HI" || tr.GetError() != "" { + t.Errorf("tag result html=%q error=%q", tr.GetHtml(), tr.GetError()) + } + + // HOOK_APPLY_FILTER dispatches to the registered filter fn. + resp = w.invoke(t, abiv1.Hook_HOOK_APPLY_FILTER, &abiv1.ApplyFilterRequest{ + FilterName: "exclaim", + Input: "hello", + }) + if resp.GetError() != nil { + t.Fatalf("APPLY_FILTER error: %v", resp.GetError()) + } + fr := &abiv1.ApplyFilterResponse{} + if err := proto.Unmarshal(resp.GetPayload(), fr); err != nil { + t.Fatalf("unmarshal ApplyFilterResponse: %v", err) + } + if fr.GetOutput() != "hello!" || fr.GetError() != "" { + t.Errorf("filter result output=%q error=%q", fr.GetOutput(), fr.GetError()) + } + + // Unknown tag → UNIMPLEMENTED AbiError. + resp = w.invoke(t, abiv1.Hook_HOOK_RENDER_TAG, &abiv1.RenderTagRequest{TagName: "nope"}) + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_UNIMPLEMENTED { + t.Errorf("unknown tag error = %v, want UNIMPLEMENTED", resp.GetError()) + } +} + +func TestWasmFixturePanicReturnsAbiErrorAndInstanceSurvives(t *testing.T) { + if testing.Short() { + t.Skip("compiles a wasm module; skipped in -short") + } + w := instantiateFixture(t) + + resp := w.invoke(t, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "wasmfixture:greeting", + ContentJson: []byte(`{"boom":true}`), + }) + e := resp.GetError() + if e.GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL { + t.Fatalf("error = %v, want INTERNAL", e) + } + if !strings.Contains(e.GetMessage(), "fixture kaboom") { + t.Errorf("error message %q does not carry the panic value", e.GetMessage()) + } + t.Logf("panic surfaced as AbiError: code=%s message=%q", e.GetCode(), e.GetMessage()) + + // The instance must remain callable after the recovered panic. + resp = w.invoke(t, abiv1.Hook_HOOK_RENDER_BLOCK, &abiv1.RenderBlockRequest{ + BlockKey: "wasmfixture:greeting", + ContentJson: []byte(`{"name":"survivor"}`), + }) + if resp.GetError() != nil { + t.Fatalf("post-panic render error: %v", resp.GetError()) + } + rb := &abiv1.RenderBlockResponse{} + if err := proto.Unmarshal(resp.GetPayload(), rb); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if rb.GetHtml() != "

hello survivor

" { + t.Errorf("post-panic html = %q", rb.GetHtml()) + } + t.Log("instance still callable after panic") +} + +func TestWasmFixtureUnknownRequestPointerIsDecodeError(t *testing.T) { + if testing.Short() { + t.Skip("compiles a wasm module; skipped in -short") + } + w := instantiateFixture(t) + + // Bypass bn_alloc: the guest must refuse pointers it did not hand out. + req, _ := proto.Marshal(&abiv1.InvokeRequest{Hook: abiv1.Hook_HOOK_DESCRIBE}) + res, err := w.mod.ExportedFunction("bn_invoke").Call(w.ctx, + uint64(abiv1.Hook_HOOK_DESCRIBE), uint64(binary.MaxVarintLen64), uint64(len(req))) + if err != nil { + t.Fatalf("bn_invoke: %v", err) + } + packed := res[0] + if packed == 0 { + t.Fatal("packed 0") + } + respBytes, ok := w.mod.Memory().Read(uint32(packed>>32), uint32(packed)) + if !ok { + t.Fatal("read response out of range") + } + resp := &abiv1.InvokeResponse{} + if err := proto.Unmarshal(respBytes, resp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if resp.GetError().GetCode() != abiv1.AbiErrorCode_ABI_ERROR_CODE_DECODE { + t.Fatalf("error = %v, want DECODE", resp.GetError()) + } +} diff --git a/rbac/role.go b/rbac/role.go new file mode 100644 index 0000000..8fbe5e9 --- /dev/null +++ b/rbac/role.go @@ -0,0 +1,44 @@ +package rbac + +type Role string + +const ( + RoleViewer Role = "viewer" + RoleAdmin Role = "admin" + RoleSuperadmin Role = "superadmin" + RolePublic Role = "public" +) + +var RoleHierarchy = map[Role]int{ + RoleViewer: 0, + RoleAdmin: 1, + RoleSuperadmin: 2, +} + +func HasPermission(userRole Role, requiredRole Role) bool { + if requiredRole == "" { + return true + } + userLevel, ok := RoleHierarchy[userRole] + if !ok { + return false + } + requiredLevel, ok := RoleHierarchy[requiredRole] + if !ok { + return false + } + return userLevel >= requiredLevel +} + +func RoleFromString(s string) Role { + switch s { + case "viewer": + return RoleViewer + case "admin": + return RoleAdmin + case "superadmin": + return RoleSuperadmin + default: + return "" + } +} diff --git a/settings/settings.go b/settings/settings.go new file mode 100644 index 0000000..7f2fc8c --- /dev/null +++ b/settings/settings.go @@ -0,0 +1,53 @@ +package settings + +import "context" + +// Settings provides settings access for plugins. +type Settings interface { + GetSiteSettings(ctx context.Context) (map[string]any, error) + GetPluginSettings(ctx context.Context, pluginName string) (map[string]any, error) +} + +// Updater allows plugins to modify site settings and their own plugin +// settings. +type Updater interface { + UpdateSiteSetting(ctx context.Context, key string, value any) error + // UpdatePluginSettings replaces the plugin's own settings map. Across the + // wasm ABI the host enforces that pluginName matches the calling plugin. + UpdatePluginSettings(ctx context.Context, pluginName string, settings map[string]any) error +} + +// GetStringOr returns a string value from a map, or defaultVal if not found/wrong type. +func GetStringOr(m map[string]any, key, defaultVal string) string { + if v, ok := m[key].(string); ok { + return v + } + return defaultVal +} + +// GetBoolOr returns a bool value from a map, or defaultVal if not found/wrong type. +func GetBoolOr(m map[string]any, key string, defaultVal bool) bool { + if v, ok := m[key].(bool); ok { + return v + } + return defaultVal +} + +// GetIntOr returns an int value from a map, handling both int and float64 (JSON numbers). +func GetIntOr(m map[string]any, key string, defaultVal int) int { + if v, ok := m[key].(float64); ok { + return int(v) + } + if v, ok := m[key].(int); ok { + return v + } + return defaultVal +} + +// GetFloat64Or returns a float64 value from a map, or defaultVal if not found/wrong type. +func GetFloat64Or(m map[string]any, key string, defaultVal float64) float64 { + if v, ok := m[key].(float64); ok { + return v + } + return defaultVal +} diff --git a/subscriptions/subscriptions.go b/subscriptions/subscriptions.go new file mode 100644 index 0000000..8d3b3d5 --- /dev/null +++ b/subscriptions/subscriptions.go @@ -0,0 +1,45 @@ +package subscriptions + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +// Subscriptions provides subscription tier and billing plan access for plugins. +type Subscriptions interface { + GetUserTierLevel(ctx context.Context, userID uuid.UUID) (*TierLevel, error) + GetTierBySlug(ctx context.Context, slug string) (*Tier, error) + ListTiers(ctx context.Context) ([]Tier, error) + ListActivePlans(ctx context.Context, tierID uuid.UUID) ([]Plan, error) +} + +// TierLevel is the resolved tier for a user. +type TierLevel struct { + Level int + Features []byte +} + +// Tier is a subscription tier definition. +type Tier struct { + ID uuid.UUID + Name string + Slug string + Level int + Description string + Features []byte + IsDefault bool + Position int +} + +// Plan is an active billing plan within a tier. +type Plan struct { + ID uuid.UUID + TierID uuid.UUID + BillingInterval string + Amount int32 + Currency string + IsActive bool + CreatedAt time.Time +} diff --git a/templates/bn/asset_hooks.go b/templates/bn/asset_hooks.go new file mode 100644 index 0000000..20525df --- /dev/null +++ b/templates/bn/asset_hooks.go @@ -0,0 +1,11 @@ +package bn + +// VersionedAssetURL resolves an asset URL to a cache-versioned form. The +// default is the identity function; the CMS host wires it to +// internal/assets.VersionedURL at startup so plugin stylesheet links get +// content-hash ?v= params. Guest-side (wasm) renders keep the identity +// default — asset hashing is a host concern. +// +// This file is part of the cms→core template sync set (make sync-templates); +// it must stay SDK-clean (no cms imports). +var VersionedAssetURL = func(url string) string { return url } diff --git a/templates/bn/engagement.templ b/templates/bn/engagement.templ new file mode 100644 index 0000000..b1a7d82 --- /dev/null +++ b/templates/bn/engagement.templ @@ -0,0 +1,514 @@ +package bn + +import ( + "encoding/json" +) + +// EngagementConfig contains configuration for the engagement tracker +type EngagementConfig struct { + PagePath string `json:"pagePath"` + PageID string `json:"pageId"` + PostWordCount int `json:"postWordCount"` + SessionID string `json:"sessionId"` + VisitorHash string `json:"visitorHash"` + IsPost bool `json:"isPost"` +} + +// engagementConfigJSON converts the config to JSON for embedding in the script +func engagementConfigJSON(config EngagementConfig) string { + data, err := json.Marshal(config) + if err != nil { + return "{}" + } + return string(data) +} + +// EngagementScript renders the blog post engagement tracking script. +// Only renders if IsPost is true to avoid bloating regular pages. +templ EngagementScript(config EngagementConfig) { + if config.IsPost && config.PageID != "" { + + } +} diff --git a/templates/bn/engagement_templ.go b/templates/bn/engagement_templ.go new file mode 100644 index 0000000..6a9d669 --- /dev/null +++ b/templates/bn/engagement_templ.go @@ -0,0 +1,80 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package bn + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "encoding/json" +) + +// EngagementConfig contains configuration for the engagement tracker +type EngagementConfig struct { + PagePath string `json:"pagePath"` + PageID string `json:"pageId"` + PostWordCount int `json:"postWordCount"` + SessionID string `json:"sessionId"` + VisitorHash string `json:"visitorHash"` + IsPost bool `json:"isPost"` +} + +// engagementConfigJSON converts the config to JSON for embedding in the script +func engagementConfigJSON(config EngagementConfig) string { + data, err := json.Marshal(config) + if err != nil { + return "{}" + } + return string(data) +} + +// EngagementScript renders the blog post engagement tracking script. +// Only renders if IsPost is true to avoid bloating regular pages. +func EngagementScript(config EngagementConfig) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if config.IsPost && config.PageID != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/templates/bn/head.templ b/templates/bn/head.templ new file mode 100644 index 0000000..cfd2e26 --- /dev/null +++ b/templates/bn/head.templ @@ -0,0 +1,961 @@ +package bn + +import ( + "strings" + "time" + + "github.com/google/uuid" +) + +// resolveMediaURL converts media: prefixed URLs to proper /media/ paths +// e.g., "media:abc/image.webp" → "/media/abc/image.webp" +func resolveMediaURL(url string) string { + if strings.HasPrefix(url, "media:") { + return "/media/" + strings.TrimPrefix(url, "media:") + } + return url +} + +// canonicalURL resolves the canonical link. An admin-set value wins (made absolute +// against the page's own origin when it is root-relative); otherwise the page's own +// absolute URL is used as a self-referencing canonical. Returns "" only when neither +// is available. +func canonicalURL(canonical, pageURL string) string { + if canonical == "" { + return pageURL + } + if strings.HasPrefix(canonical, "http://") || strings.HasPrefix(canonical, "https://") { + return canonical + } + if strings.HasPrefix(canonical, "/") && pageURL != "" { + if i := strings.Index(pageURL, "://"); i >= 0 { + if j := strings.IndexByte(pageURL[i+3:], '/'); j >= 0 { + return pageURL[:i+3+j] + canonical + } + return pageURL + canonical + } + } + return canonical +} + +// BrandingData contains generated favicon/icon URLs +type BrandingData struct { + FaviconICO string // /.brand/{id}/favicon.ico + Favicon16 string // /.brand/{id}/favicon-16x16.png + Favicon32 string // /.brand/{id}/favicon-32x32.png + Favicon96 string // /.brand/{id}/favicon-96x96.png + AppleTouchIcon string // /.brand/{id}/apple-touch-icon.png (180x180) + Android192 string // /.brand/{id}/android-chrome-192x192.png + Android512 string // /.brand/{id}/android-chrome-512x512.png + Maskable512 string // /.brand/{id}/maskable-512x512.png + Master1024 string // /.brand/{id}/icon-1024x1024.png + ManifestURL string // /.brand/{id}/site.webmanifest + MaskIcon string // /.brand/{id}/mask-icon.svg + MSTile150 string // /.brand/{id}/mstile-150x150.png + BrowserConfig string // /.brand/{id}/browserconfig.xml + ThemeColor string + SVG string // Optional SVG pass-through + IsGenerated bool +} + +// CustomScriptEntry represents a single custom script with placement control +type CustomScriptEntry struct { + Name string + Placement string // "head" or "body" + Code string + Enabled bool +} + +// SiteSettingsData contains site-wide settings for head injection +type SiteSettingsData struct { + Title string + Description string + Favicon string + Logo string + LogoAlt string + AppleTouchIcon string + GoogleAnalyticsID string + CustomScripts []CustomScriptEntry + GoogleAnalyticsEnabled bool + GoogleAnalyticsExplicitlySet bool // true if the enabled flag was explicitly set in JSON (not nil) + MetaDescription string + DefaultOGImage string + TwitterHandle string + OGSiteName string + Branding BrandingData + AdminBypassMode string // "maintenance", "coming_soon", or "" if not bypassing + Toolbar ToolbarData + LLMsTxtEnabled bool // Whether llms.txt is enabled for AI content discovery + RSSFeedURL string // URL to the RSS feed (e.g., "/rss"), empty to disable + RSSFeedTitle string // Feed title for discovery link +} + +// PageMeta contains page-level SEO meta data (overrides site defaults) +type PageMeta struct { + MetaTitle string // Custom SEO title (overrides page title) + MetaDescription string // Page-specific meta description + OGTitle string // Open Graph title + OGDescription string // Open Graph description + OGImage string // Open Graph image URL + TwitterTitle string // Twitter card title + TwitterDescription string // Twitter card description + TwitterImage string // Twitter card image URL + CanonicalURL string // Canonical URL for this page + RobotsDirective string // Robots directive (e.g., "noindex, nofollow") + OGType string // Open Graph type ("article" for posts, else "website") + PageURL string // Absolute URL of this page (og:url + auto-canonical) + ArticlePublishedTime string // ISO 8601 published time (articles only) + ArticleAuthor string // Author name (articles only) +} + +// HeadData contains all data needed to render the element +type HeadData struct { + Title string + Settings SiteSettingsData + PageMeta PageMeta // Page-level SEO overrides + ThemeCSS string + ThemeMode string // "light", "dark", or "system" + PluginStyles []string // Additional stylesheet URLs + StructuredData string // JSON-LD structured data + CSSHash string // Cache-busting hash for styles.css + PageviewNonce string // Unique nonce for pageview deduplication + EngagementConfig EngagementConfig // Engagement tracking config (for blog posts) +} + +// ParseEngagementConfig extracts engagement config from the document map +func ParseEngagementConfig(doc map[string]any) EngagementConfig { + config := EngagementConfig{} + + engData, ok := doc["engagement_config"].(map[string]any) + if !ok { + return config + } + + if v, ok := engData["page_path"].(string); ok { + config.PagePath = v + } + if v, ok := engData["page_id"].(string); ok { + config.PageID = v + } + if v, ok := engData["post_word_count"].(int); ok { + config.PostWordCount = v + } + if v, ok := engData["session_id"].(string); ok { + config.SessionID = v + } + if v, ok := engData["visitor_hash"].(string); ok { + config.VisitorHash = v + } + if v, ok := engData["is_post"].(bool); ok { + config.IsPost = v + } + + return config +} + +// ParseSiteSettings extracts site settings from the document map +func ParseSiteSettings(doc map[string]any) SiteSettingsData { + settings := SiteSettingsData{} + + siteData, ok := doc["site_settings"].(map[string]any) + if !ok { + return settings + } + + if v, ok := siteData["title"].(string); ok { + settings.Title = v + } + if v, ok := siteData["description"].(string); ok { + settings.Description = v + } + if v, ok := siteData["favicon"].(string); ok { + settings.Favicon = v + } + if v, ok := siteData["logo"].(string); ok { + settings.Logo = v + } + if v, ok := siteData["logo_alt"].(string); ok { + settings.LogoAlt = v + } + if v, ok := siteData["apple_touch_icon"].(string); ok { + settings.AppleTouchIcon = v + } + + // SEO defaults + if seoData, ok := siteData["seo_defaults"].(map[string]any); ok { + if v, ok := seoData["meta_description"].(string); ok { + settings.MetaDescription = v + } + if v, ok := seoData["default_og_image"].(string); ok { + settings.DefaultOGImage = v + } + if v, ok := seoData["twitter_handle"].(string); ok { + settings.TwitterHandle = v + } + if v, ok := seoData["og_site_name"].(string); ok { + settings.OGSiteName = v + } + } + + // Analytics + var legacyHeadScripts, legacyBodyScripts string + if analyticsData, ok := siteData["analytics"].(map[string]any); ok { + if v, ok := analyticsData["google_analytics_id"].(string); ok { + settings.GoogleAnalyticsID = v + } + // Read GA enabled flag — if present, use it; if absent but ID is set, default to enabled (backward compat) + if v, ok := analyticsData["google_analytics_enabled"].(bool); ok { + settings.GoogleAnalyticsEnabled = v + settings.GoogleAnalyticsExplicitlySet = true + } else if settings.GoogleAnalyticsID != "" { + settings.GoogleAnalyticsEnabled = true + } + // Preserve legacy flat fields for fallback + if v, ok := analyticsData["custom_head_scripts"].(string); ok { + legacyHeadScripts = v + } + if v, ok := analyticsData["custom_body_scripts"].(string); ok { + legacyBodyScripts = v + } + } + + // Structured custom scripts + if scriptsArr, ok := siteData["custom_scripts"].([]any); ok && len(scriptsArr) > 0 { + for _, item := range scriptsArr { + entry, ok := item.(map[string]any) + if !ok { + continue + } + cs := CustomScriptEntry{} + if v, ok := entry["name"].(string); ok { + cs.Name = v + } + // Placement: proto enum stored as float64 (1=head, 2=body) or string + switch p := entry["placement"].(type) { + case float64: + if p == 1 { + cs.Placement = "head" + } else if p == 2 { + cs.Placement = "body" + } + case string: + cs.Placement = p + } + if v, ok := entry["code"].(string); ok { + cs.Code = v + } + if v, ok := entry["enabled"].(bool); ok { + cs.Enabled = v + } + settings.CustomScripts = append(settings.CustomScripts, cs) + } + } + // Fallback: if no structured scripts, migrate old flat fields + if len(settings.CustomScripts) == 0 { + if legacyHeadScripts != "" { + settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{ + Name: "Legacy Head Scripts", + Placement: "head", + Code: legacyHeadScripts, + Enabled: true, + }) + } + if legacyBodyScripts != "" { + settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{ + Name: "Legacy Body Scripts", + Placement: "body", + Code: legacyBodyScripts, + Enabled: true, + }) + } + } + + // Branding (generated favicons) + if brandingData, ok := siteData["branding"].(map[string]any); ok { + if v, ok := brandingData["is_generated"].(bool); ok { + settings.Branding.IsGenerated = v + } + if v, ok := brandingData["favicon_ico"].(string); ok { + settings.Branding.FaviconICO = v + } + if v, ok := brandingData["favicon_16"].(string); ok { + settings.Branding.Favicon16 = v + } + if v, ok := brandingData["favicon_32"].(string); ok { + settings.Branding.Favicon32 = v + } + if v, ok := brandingData["apple_touch_icon"].(string); ok { + settings.Branding.AppleTouchIcon = v + } + if v, ok := brandingData["android_192"].(string); ok { + settings.Branding.Android192 = v + } + if v, ok := brandingData["android_512"].(string); ok { + settings.Branding.Android512 = v + } + if v, ok := brandingData["maskable_512"].(string); ok { + settings.Branding.Maskable512 = v + } + if v, ok := brandingData["master_1024"].(string); ok { + settings.Branding.Master1024 = v + } + if v, ok := brandingData["manifest_url"].(string); ok { + settings.Branding.ManifestURL = v + } + if v, ok := brandingData["theme_color"].(string); ok { + settings.Branding.ThemeColor = v + } + if v, ok := brandingData["svg"].(string); ok { + settings.Branding.SVG = v + } + if v, ok := brandingData["favicon_96"].(string); ok { + settings.Branding.Favicon96 = v + } + if v, ok := brandingData["mask_icon"].(string); ok { + settings.Branding.MaskIcon = v + } + if v, ok := brandingData["mstile_150"].(string); ok { + settings.Branding.MSTile150 = v + } + if v, ok := brandingData["browserconfig"].(string); ok { + settings.Branding.BrowserConfig = v + } + } + + // Admin bypass mode (for showing banner when admin bypasses maintenance/coming_soon) + if v, ok := siteData["admin_bypass_mode"].(string); ok { + settings.AdminBypassMode = v + } + + // Admin editor toolbar + if toolbarData, ok := siteData["toolbar"].(map[string]any); ok { + settings.Toolbar = ParseToolbarData(toolbarData) + } + + // AI Optimization settings + if aiOpt, ok := siteData["aiOptimization"].(map[string]any); ok { + if v, ok := aiOpt["llmsTxtEnabled"].(bool); ok { + settings.LLMsTxtEnabled = v + } + } + + // RSS feed auto-discovery (injected by page handler from system page) + if v, ok := siteData["rss_feed_url"].(string); ok { + settings.RSSFeedURL = v + } + if v, ok := siteData["rss_feed_title"].(string); ok { + settings.RSSFeedTitle = v + } + + return settings +} + +// ParsePageMeta extracts page-level SEO metadata from the document map +// These fields override site-level defaults when set +func ParsePageMeta(doc map[string]any) PageMeta { + meta := PageMeta{} + + // Page-level SEO fields are stored directly in the document root + // (matching frontend PageInfo type in web/src/store/editor.ts) + if v, ok := doc["metaTitle"].(string); ok { + meta.MetaTitle = v + } + if v, ok := doc["metaDescription"].(string); ok { + meta.MetaDescription = v + } + if v, ok := doc["ogTitle"].(string); ok { + meta.OGTitle = v + } + if v, ok := doc["ogDescription"].(string); ok { + meta.OGDescription = v + } + if v, ok := doc["ogImage"].(string); ok { + meta.OGImage = v + } + if v, ok := doc["twitterTitle"].(string); ok { + meta.TwitterTitle = v + } + if v, ok := doc["twitterDescription"].(string); ok { + meta.TwitterDescription = v + } + if v, ok := doc["twitterImage"].(string); ok { + meta.TwitterImage = v + } + if v, ok := doc["canonicalUrl"].(string); ok { + meta.CanonicalURL = v + } + if v, ok := doc["robotsDirective"].(string); ok { + meta.RobotsDirective = v + } + if v, ok := doc["ogType"].(string); ok { + meta.OGType = v + } + if v, ok := doc["pageUrl"].(string); ok { + meta.PageURL = v + } + if v, ok := doc["articlePublishedTime"].(string); ok { + meta.ArticlePublishedTime = v + } + if v, ok := doc["articleAuthor"].(string); ok { + meta.ArticleAuthor = v + } + + return meta +} + +// ParseToolbarData extracts toolbar data from the document map +func ParseToolbarData(data map[string]any) ToolbarData { + toolbar := ToolbarData{} + + if v, ok := data["enabled"].(bool); ok { + toolbar.Enabled = v + } + if v, ok := data["page_id"].(string); ok { + if id, err := uuid.Parse(v); err == nil { + toolbar.PageID = id + } + } + if v, ok := data["page_slug"].(string); ok { + toolbar.PageSlug = v + } + if v, ok := data["page_title"].(string); ok { + toolbar.PageTitle = v + } + if v, ok := data["post_type"].(string); ok { + toolbar.PostType = v + } + if v, ok := data["status"].(string); ok { + toolbar.Status = v + } + if v, ok := data["has_unpublished_changes"].(bool); ok { + toolbar.HasUnpublishedChanges = v + } + if v, ok := data["preview_mode"].(string); ok { + toolbar.PreviewMode = v + } + if v, ok := data["hide_preview_toggle"].(bool); ok { + toolbar.HidePreviewToggle = v + } + if v, ok := data["animate"].(bool); ok { + toolbar.Animate = v + } + if v, ok := data["position"].(string); ok { + toolbar.Position = v + } + if v, ok := data["template_name"].(string); ok { + toolbar.TemplateName = v + } + if v, ok := data["author_name"].(string); ok { + toolbar.AuthorName = v + } + if v, ok := data["author_slug"].(string); ok { + toolbar.AuthorSlug = v + } + if v, ok := data["last_modified"].(time.Time); ok { + toolbar.LastModified = v + } + if v, ok := data["edit_url"].(string); ok { + toolbar.EditURL = v + } + if v, ok := data["settings_url"].(string); ok { + toolbar.SettingsURL = v + } + if v, ok := data["history_url"].(string); ok { + toolbar.HistoryURL = v + } + if v, ok := data["analytics_url"].(string); ok { + toolbar.AnalyticsURL = v + } + + // Analytics snapshot + if v, ok := data["today_pageviews"].(int64); ok { + toolbar.TodayPageviews = v + } + if v, ok := data["pageviews_trend"].(string); ok { + toolbar.PageviewsTrend = v + } + if v, ok := data["trend_percent"].(int); ok { + toolbar.TrendPercent = v + } + + // Blog-specific fields + if v, ok := data["reading_time"].(int); ok { + toolbar.ReadingTime = v + } + if v, ok := data["word_count"].(int); ok { + toolbar.WordCount = v + } + if v, ok := data["category_count"].(int); ok { + toolbar.CategoryCount = v + } + + return toolbar +} + +// EffectiveTitle returns the title to use in the tag. +// Priority: PageMeta.MetaTitle → Page Title | Site Name → Page Title +func (d HeadData) EffectiveTitle() string { + if d.PageMeta.MetaTitle != "" { + return d.PageMeta.MetaTitle + } + if d.Settings.OGSiteName != "" && d.Title != "" { + return d.Title + " | " + d.Settings.OGSiteName + } + return d.Title +} + +// Head renders the complete <head> element with site settings and plugin extensions. +templ Head(data HeadData) { + @recordValidationCall(ctx, "Head") + <head> + <meta charset="UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <title>{ data.EffectiveTitle() } + if data.CSSHash != "" { + + } + @themeInitScript(data.ThemeMode) + + if data.Settings.Branding.IsGenerated { + // Use generated brand assets (modern RFG-parity set; SVG first) + if data.Settings.Branding.SVG != "" { + + } + if data.Settings.Branding.Favicon96 != "" { + + } + + + + + if data.Settings.Branding.MaskIcon != "" { + + } + + if data.Settings.Branding.ThemeColor != "" { + + } + if data.Settings.Branding.BrowserConfig != "" { + + } + if data.Title != "" { + + } + } else { + // Legacy fallback - manual favicon uploads + if data.Settings.Favicon != "" { + + } + if data.Settings.AppleTouchIcon != "" { + + } + } + // === SEO META TAGS === + // Fallback chain: Page-level → Site defaults + + // Meta description: Page override → Site default + if data.PageMeta.MetaDescription != "" { + + } else if data.Settings.MetaDescription != "" { + + } + + // Canonical URL: admin override (resolved to absolute) → auto self-canonical (M8) + if canonical := canonicalURL(data.PageMeta.CanonicalURL, data.PageMeta.PageURL); canonical != "" { + + } + + // Robots directive (page-level only - defaults to index,follow if not set) + if data.PageMeta.RobotsDirective != "" { + + } + + // === OPEN GRAPH META TAGS === + // og:site_name is always site-level + if data.Settings.OGSiteName != "" { + + } + + // og:title: Page OG → Page Meta Title → Page Title + if data.PageMeta.OGTitle != "" { + + } else if data.PageMeta.MetaTitle != "" { + + } else if data.Title != "" { + + } + + // og:description: Page OG → Page Meta Description → Site default + if data.PageMeta.OGDescription != "" { + + } else if data.PageMeta.MetaDescription != "" { + + } else if data.Settings.MetaDescription != "" { + + } + + // og:image: Page OG → Site default (resolve media: URLs) + if data.PageMeta.OGImage != "" { + + } else if data.Settings.DefaultOGImage != "" { + + } + + // og:type: article for posts, website otherwise (M5) + if data.PageMeta.OGType != "" { + + } else { + + } + // og:url: absolute URL of this page (M6) + if data.PageMeta.PageURL != "" { + + } + // article:* metadata for posts (M5) + if data.PageMeta.OGType == "article" { + if data.PageMeta.ArticlePublishedTime != "" { + + } + if data.PageMeta.ArticleAuthor != "" { + + } + } + + // === TWITTER CARD META TAGS === + if data.Settings.TwitterHandle != "" { + + } + + + // twitter:title: Page Twitter → Page OG → Page Meta Title → Page Title + if data.PageMeta.TwitterTitle != "" { + + } else if data.PageMeta.OGTitle != "" { + + } else if data.PageMeta.MetaTitle != "" { + + } else if data.Title != "" { + + } + + // twitter:description: Page Twitter → Page OG → Page Meta Description → Site default + if data.PageMeta.TwitterDescription != "" { + + } else if data.PageMeta.OGDescription != "" { + + } else if data.PageMeta.MetaDescription != "" { + + } else if data.Settings.MetaDescription != "" { + + } + + // twitter:image: Page Twitter → Page OG → Site default (resolve media: URLs) + if data.PageMeta.TwitterImage != "" { + + } else if data.PageMeta.OGImage != "" { + + } else if data.Settings.DefaultOGImage != "" { + + } + + // === AI CONTENT DISCOVERY === + // llms.txt meta tag for AI crawlers (if enabled) + if data.Settings.LLMsTxtEnabled { + + + } + + // === RSS/ATOM FEED DISCOVERY === + if data.Settings.RSSFeedURL != "" { + + + } + + if data.Settings.GoogleAnalyticsID != "" && (data.Settings.GoogleAnalyticsEnabled || !data.Settings.GoogleAnalyticsExplicitlySet) { + `) +} + +// AdminBypassBanner renders a banner when admin is bypassing maintenance/coming_soon mode +// This should be rendered at the very start of the to push down all content +templ AdminBypassBanner(settings SiteSettingsData) { + if settings.AdminBypassMode != "" { +
+
+ + + + if settings.AdminBypassMode == "maintenance" { + + Maintenance Mode Active — You're seeing the normal site because you're logged in as admin. + Manage + + } else if settings.AdminBypassMode == "coming_soon" { + + Coming Soon Mode Active — You're seeing the normal site because you're logged in as admin. + Manage + + } +
+
+ } +} + +// BodyEnd renders custom scripts and admin toolbar before +templ BodyEnd(settings SiteSettingsData) { + @recordValidationCall(ctx, "BodyEnd") + for _, script := range settings.CustomScripts { + if script.Enabled && script.Placement == "body" { + @templ.Raw(script.Code) + } + } + // Render admin editor toolbar if enabled (auto-injects for all templates) + @AdminEditorToolbar(settings.Toolbar) + { children... } +} + +// themeStyle renders the theme CSS variables +func themeStyleComponent(css string) templ.Component { + return templ.Raw(``) +} + +templ themeStyle(css string) { + @themeStyleComponent(css) +} + +// googleAnalyticsScript renders the GA4 inline script +func googleAnalyticsScript(gaID string) templ.Component { + return templ.Raw(``) +} + +// analyticsScript renders the built-in analytics tracking script (cookie-less) +// nonce is used for deduplication with server-side tracking +templ analyticsScript(nonce string) { + +} + +func themeInitScript(themeMode string) templ.Component { + switch themeMode { + case "light", "dark", "system": + default: + themeMode = "light" + } + // Precedence: admin toolbar override (per-tab, sessionStorage) → visitor + // preference (bn-theme cookie/localStorage) → site default → system. + // Exposed as window.bnApplyTheme so the toolbar theme tester can re-apply + // after changing the override without duplicating this resolution. + return templ.Raw(``) +} diff --git a/templates/bn/head_templ.go b/templates/bn/head_templ.go new file mode 100644 index 0000000..d723d23 --- /dev/null +++ b/templates/bn/head_templ.go @@ -0,0 +1,1761 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package bn + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "strings" + "time" + + "github.com/google/uuid" +) + +// resolveMediaURL converts media: prefixed URLs to proper /media/ paths +// e.g., "media:abc/image.webp" → "/media/abc/image.webp" +func resolveMediaURL(url string) string { + if strings.HasPrefix(url, "media:") { + return "/media/" + strings.TrimPrefix(url, "media:") + } + return url +} + +// canonicalURL resolves the canonical link. An admin-set value wins (made absolute +// against the page's own origin when it is root-relative); otherwise the page's own +// absolute URL is used as a self-referencing canonical. Returns "" only when neither +// is available. +func canonicalURL(canonical, pageURL string) string { + if canonical == "" { + return pageURL + } + if strings.HasPrefix(canonical, "http://") || strings.HasPrefix(canonical, "https://") { + return canonical + } + if strings.HasPrefix(canonical, "/") && pageURL != "" { + if i := strings.Index(pageURL, "://"); i >= 0 { + if j := strings.IndexByte(pageURL[i+3:], '/'); j >= 0 { + return pageURL[:i+3+j] + canonical + } + return pageURL + canonical + } + } + return canonical +} + +// BrandingData contains generated favicon/icon URLs +type BrandingData struct { + FaviconICO string // /.brand/{id}/favicon.ico + Favicon16 string // /.brand/{id}/favicon-16x16.png + Favicon32 string // /.brand/{id}/favicon-32x32.png + Favicon96 string // /.brand/{id}/favicon-96x96.png + AppleTouchIcon string // /.brand/{id}/apple-touch-icon.png (180x180) + Android192 string // /.brand/{id}/android-chrome-192x192.png + Android512 string // /.brand/{id}/android-chrome-512x512.png + Maskable512 string // /.brand/{id}/maskable-512x512.png + Master1024 string // /.brand/{id}/icon-1024x1024.png + ManifestURL string // /.brand/{id}/site.webmanifest + MaskIcon string // /.brand/{id}/mask-icon.svg + MSTile150 string // /.brand/{id}/mstile-150x150.png + BrowserConfig string // /.brand/{id}/browserconfig.xml + ThemeColor string + SVG string // Optional SVG pass-through + IsGenerated bool +} + +// CustomScriptEntry represents a single custom script with placement control +type CustomScriptEntry struct { + Name string + Placement string // "head" or "body" + Code string + Enabled bool +} + +// SiteSettingsData contains site-wide settings for head injection +type SiteSettingsData struct { + Title string + Description string + Favicon string + Logo string + LogoAlt string + AppleTouchIcon string + GoogleAnalyticsID string + CustomScripts []CustomScriptEntry + GoogleAnalyticsEnabled bool + GoogleAnalyticsExplicitlySet bool // true if the enabled flag was explicitly set in JSON (not nil) + MetaDescription string + DefaultOGImage string + TwitterHandle string + OGSiteName string + Branding BrandingData + AdminBypassMode string // "maintenance", "coming_soon", or "" if not bypassing + Toolbar ToolbarData + LLMsTxtEnabled bool // Whether llms.txt is enabled for AI content discovery + RSSFeedURL string // URL to the RSS feed (e.g., "/rss"), empty to disable + RSSFeedTitle string // Feed title for discovery link +} + +// PageMeta contains page-level SEO meta data (overrides site defaults) +type PageMeta struct { + MetaTitle string // Custom SEO title (overrides page title) + MetaDescription string // Page-specific meta description + OGTitle string // Open Graph title + OGDescription string // Open Graph description + OGImage string // Open Graph image URL + TwitterTitle string // Twitter card title + TwitterDescription string // Twitter card description + TwitterImage string // Twitter card image URL + CanonicalURL string // Canonical URL for this page + RobotsDirective string // Robots directive (e.g., "noindex, nofollow") + OGType string // Open Graph type ("article" for posts, else "website") + PageURL string // Absolute URL of this page (og:url + auto-canonical) + ArticlePublishedTime string // ISO 8601 published time (articles only) + ArticleAuthor string // Author name (articles only) +} + +// HeadData contains all data needed to render the element +type HeadData struct { + Title string + Settings SiteSettingsData + PageMeta PageMeta // Page-level SEO overrides + ThemeCSS string + ThemeMode string // "light", "dark", or "system" + PluginStyles []string // Additional stylesheet URLs + StructuredData string // JSON-LD structured data + CSSHash string // Cache-busting hash for styles.css + PageviewNonce string // Unique nonce for pageview deduplication + EngagementConfig EngagementConfig // Engagement tracking config (for blog posts) +} + +// ParseEngagementConfig extracts engagement config from the document map +func ParseEngagementConfig(doc map[string]any) EngagementConfig { + config := EngagementConfig{} + + engData, ok := doc["engagement_config"].(map[string]any) + if !ok { + return config + } + + if v, ok := engData["page_path"].(string); ok { + config.PagePath = v + } + if v, ok := engData["page_id"].(string); ok { + config.PageID = v + } + if v, ok := engData["post_word_count"].(int); ok { + config.PostWordCount = v + } + if v, ok := engData["session_id"].(string); ok { + config.SessionID = v + } + if v, ok := engData["visitor_hash"].(string); ok { + config.VisitorHash = v + } + if v, ok := engData["is_post"].(bool); ok { + config.IsPost = v + } + + return config +} + +// ParseSiteSettings extracts site settings from the document map +func ParseSiteSettings(doc map[string]any) SiteSettingsData { + settings := SiteSettingsData{} + + siteData, ok := doc["site_settings"].(map[string]any) + if !ok { + return settings + } + + if v, ok := siteData["title"].(string); ok { + settings.Title = v + } + if v, ok := siteData["description"].(string); ok { + settings.Description = v + } + if v, ok := siteData["favicon"].(string); ok { + settings.Favicon = v + } + if v, ok := siteData["logo"].(string); ok { + settings.Logo = v + } + if v, ok := siteData["logo_alt"].(string); ok { + settings.LogoAlt = v + } + if v, ok := siteData["apple_touch_icon"].(string); ok { + settings.AppleTouchIcon = v + } + + // SEO defaults + if seoData, ok := siteData["seo_defaults"].(map[string]any); ok { + if v, ok := seoData["meta_description"].(string); ok { + settings.MetaDescription = v + } + if v, ok := seoData["default_og_image"].(string); ok { + settings.DefaultOGImage = v + } + if v, ok := seoData["twitter_handle"].(string); ok { + settings.TwitterHandle = v + } + if v, ok := seoData["og_site_name"].(string); ok { + settings.OGSiteName = v + } + } + + // Analytics + var legacyHeadScripts, legacyBodyScripts string + if analyticsData, ok := siteData["analytics"].(map[string]any); ok { + if v, ok := analyticsData["google_analytics_id"].(string); ok { + settings.GoogleAnalyticsID = v + } + // Read GA enabled flag — if present, use it; if absent but ID is set, default to enabled (backward compat) + if v, ok := analyticsData["google_analytics_enabled"].(bool); ok { + settings.GoogleAnalyticsEnabled = v + settings.GoogleAnalyticsExplicitlySet = true + } else if settings.GoogleAnalyticsID != "" { + settings.GoogleAnalyticsEnabled = true + } + // Preserve legacy flat fields for fallback + if v, ok := analyticsData["custom_head_scripts"].(string); ok { + legacyHeadScripts = v + } + if v, ok := analyticsData["custom_body_scripts"].(string); ok { + legacyBodyScripts = v + } + } + + // Structured custom scripts + if scriptsArr, ok := siteData["custom_scripts"].([]any); ok && len(scriptsArr) > 0 { + for _, item := range scriptsArr { + entry, ok := item.(map[string]any) + if !ok { + continue + } + cs := CustomScriptEntry{} + if v, ok := entry["name"].(string); ok { + cs.Name = v + } + // Placement: proto enum stored as float64 (1=head, 2=body) or string + switch p := entry["placement"].(type) { + case float64: + if p == 1 { + cs.Placement = "head" + } else if p == 2 { + cs.Placement = "body" + } + case string: + cs.Placement = p + } + if v, ok := entry["code"].(string); ok { + cs.Code = v + } + if v, ok := entry["enabled"].(bool); ok { + cs.Enabled = v + } + settings.CustomScripts = append(settings.CustomScripts, cs) + } + } + // Fallback: if no structured scripts, migrate old flat fields + if len(settings.CustomScripts) == 0 { + if legacyHeadScripts != "" { + settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{ + Name: "Legacy Head Scripts", + Placement: "head", + Code: legacyHeadScripts, + Enabled: true, + }) + } + if legacyBodyScripts != "" { + settings.CustomScripts = append(settings.CustomScripts, CustomScriptEntry{ + Name: "Legacy Body Scripts", + Placement: "body", + Code: legacyBodyScripts, + Enabled: true, + }) + } + } + + // Branding (generated favicons) + if brandingData, ok := siteData["branding"].(map[string]any); ok { + if v, ok := brandingData["is_generated"].(bool); ok { + settings.Branding.IsGenerated = v + } + if v, ok := brandingData["favicon_ico"].(string); ok { + settings.Branding.FaviconICO = v + } + if v, ok := brandingData["favicon_16"].(string); ok { + settings.Branding.Favicon16 = v + } + if v, ok := brandingData["favicon_32"].(string); ok { + settings.Branding.Favicon32 = v + } + if v, ok := brandingData["apple_touch_icon"].(string); ok { + settings.Branding.AppleTouchIcon = v + } + if v, ok := brandingData["android_192"].(string); ok { + settings.Branding.Android192 = v + } + if v, ok := brandingData["android_512"].(string); ok { + settings.Branding.Android512 = v + } + if v, ok := brandingData["maskable_512"].(string); ok { + settings.Branding.Maskable512 = v + } + if v, ok := brandingData["master_1024"].(string); ok { + settings.Branding.Master1024 = v + } + if v, ok := brandingData["manifest_url"].(string); ok { + settings.Branding.ManifestURL = v + } + if v, ok := brandingData["theme_color"].(string); ok { + settings.Branding.ThemeColor = v + } + if v, ok := brandingData["svg"].(string); ok { + settings.Branding.SVG = v + } + if v, ok := brandingData["favicon_96"].(string); ok { + settings.Branding.Favicon96 = v + } + if v, ok := brandingData["mask_icon"].(string); ok { + settings.Branding.MaskIcon = v + } + if v, ok := brandingData["mstile_150"].(string); ok { + settings.Branding.MSTile150 = v + } + if v, ok := brandingData["browserconfig"].(string); ok { + settings.Branding.BrowserConfig = v + } + } + + // Admin bypass mode (for showing banner when admin bypasses maintenance/coming_soon) + if v, ok := siteData["admin_bypass_mode"].(string); ok { + settings.AdminBypassMode = v + } + + // Admin editor toolbar + if toolbarData, ok := siteData["toolbar"].(map[string]any); ok { + settings.Toolbar = ParseToolbarData(toolbarData) + } + + // AI Optimization settings + if aiOpt, ok := siteData["aiOptimization"].(map[string]any); ok { + if v, ok := aiOpt["llmsTxtEnabled"].(bool); ok { + settings.LLMsTxtEnabled = v + } + } + + // RSS feed auto-discovery (injected by page handler from system page) + if v, ok := siteData["rss_feed_url"].(string); ok { + settings.RSSFeedURL = v + } + if v, ok := siteData["rss_feed_title"].(string); ok { + settings.RSSFeedTitle = v + } + + return settings +} + +// ParsePageMeta extracts page-level SEO metadata from the document map +// These fields override site-level defaults when set +func ParsePageMeta(doc map[string]any) PageMeta { + meta := PageMeta{} + + // Page-level SEO fields are stored directly in the document root + // (matching frontend PageInfo type in web/src/store/editor.ts) + if v, ok := doc["metaTitle"].(string); ok { + meta.MetaTitle = v + } + if v, ok := doc["metaDescription"].(string); ok { + meta.MetaDescription = v + } + if v, ok := doc["ogTitle"].(string); ok { + meta.OGTitle = v + } + if v, ok := doc["ogDescription"].(string); ok { + meta.OGDescription = v + } + if v, ok := doc["ogImage"].(string); ok { + meta.OGImage = v + } + if v, ok := doc["twitterTitle"].(string); ok { + meta.TwitterTitle = v + } + if v, ok := doc["twitterDescription"].(string); ok { + meta.TwitterDescription = v + } + if v, ok := doc["twitterImage"].(string); ok { + meta.TwitterImage = v + } + if v, ok := doc["canonicalUrl"].(string); ok { + meta.CanonicalURL = v + } + if v, ok := doc["robotsDirective"].(string); ok { + meta.RobotsDirective = v + } + if v, ok := doc["ogType"].(string); ok { + meta.OGType = v + } + if v, ok := doc["pageUrl"].(string); ok { + meta.PageURL = v + } + if v, ok := doc["articlePublishedTime"].(string); ok { + meta.ArticlePublishedTime = v + } + if v, ok := doc["articleAuthor"].(string); ok { + meta.ArticleAuthor = v + } + + return meta +} + +// ParseToolbarData extracts toolbar data from the document map +func ParseToolbarData(data map[string]any) ToolbarData { + toolbar := ToolbarData{} + + if v, ok := data["enabled"].(bool); ok { + toolbar.Enabled = v + } + if v, ok := data["page_id"].(string); ok { + if id, err := uuid.Parse(v); err == nil { + toolbar.PageID = id + } + } + if v, ok := data["page_slug"].(string); ok { + toolbar.PageSlug = v + } + if v, ok := data["page_title"].(string); ok { + toolbar.PageTitle = v + } + if v, ok := data["post_type"].(string); ok { + toolbar.PostType = v + } + if v, ok := data["status"].(string); ok { + toolbar.Status = v + } + if v, ok := data["has_unpublished_changes"].(bool); ok { + toolbar.HasUnpublishedChanges = v + } + if v, ok := data["preview_mode"].(string); ok { + toolbar.PreviewMode = v + } + if v, ok := data["hide_preview_toggle"].(bool); ok { + toolbar.HidePreviewToggle = v + } + if v, ok := data["animate"].(bool); ok { + toolbar.Animate = v + } + if v, ok := data["position"].(string); ok { + toolbar.Position = v + } + if v, ok := data["template_name"].(string); ok { + toolbar.TemplateName = v + } + if v, ok := data["author_name"].(string); ok { + toolbar.AuthorName = v + } + if v, ok := data["author_slug"].(string); ok { + toolbar.AuthorSlug = v + } + if v, ok := data["last_modified"].(time.Time); ok { + toolbar.LastModified = v + } + if v, ok := data["edit_url"].(string); ok { + toolbar.EditURL = v + } + if v, ok := data["settings_url"].(string); ok { + toolbar.SettingsURL = v + } + if v, ok := data["history_url"].(string); ok { + toolbar.HistoryURL = v + } + if v, ok := data["analytics_url"].(string); ok { + toolbar.AnalyticsURL = v + } + + // Analytics snapshot + if v, ok := data["today_pageviews"].(int64); ok { + toolbar.TodayPageviews = v + } + if v, ok := data["pageviews_trend"].(string); ok { + toolbar.PageviewsTrend = v + } + if v, ok := data["trend_percent"].(int); ok { + toolbar.TrendPercent = v + } + + // Blog-specific fields + if v, ok := data["reading_time"].(int); ok { + toolbar.ReadingTime = v + } + if v, ok := data["word_count"].(int); ok { + toolbar.WordCount = v + } + if v, ok := data["category_count"].(int); ok { + toolbar.CategoryCount = v + } + + return toolbar +} + +// EffectiveTitle returns the title to use in the tag. +// Priority: PageMeta.MetaTitle → Page Title | Site Name → Page Title +func (d HeadData) EffectiveTitle() string { + if d.PageMeta.MetaTitle != "" { + return d.PageMeta.MetaTitle + } + if d.Settings.OGSiteName != "" && d.Title != "" { + return d.Title + " | " + d.Settings.OGSiteName + } + return d.Title +} + +// Head renders the complete <head> element with site settings and plugin extensions. +func Head(data HeadData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = recordValidationCall(ctx, "Head").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.EffectiveTitle()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `head.templ`, Line: 513, Col: 32} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.CSSHash != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = themeInitScript(data.ThemeMode).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.Branding.IsGenerated { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.Branding.SVG != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.Branding.Favicon96 != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.Branding.MaskIcon != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.Branding.ThemeColor != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.Branding.BrowserConfig != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.Favicon != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Settings.AppleTouchIcon != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + if data.PageMeta.MetaDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Settings.MetaDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if canonical := canonicalURL(data.PageMeta.CanonicalURL, data.PageMeta.PageURL); canonical != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.RobotsDirective != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Settings.OGSiteName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.OGTitle != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.PageMeta.MetaTitle != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.OGDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.PageMeta.MetaDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Settings.MetaDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.OGImage != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Settings.DefaultOGImage != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.OGType != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.PageURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.OGType == "article" { + if data.PageMeta.ArticlePublishedTime != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.PageMeta.ArticleAuthor != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + if data.Settings.TwitterHandle != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.PageMeta.TwitterTitle != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.PageMeta.OGTitle != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.PageMeta.MetaTitle != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Title != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.TwitterDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.PageMeta.OGDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.PageMeta.MetaDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Settings.MetaDescription != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.PageMeta.TwitterImage != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.PageMeta.OGImage != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if data.Settings.DefaultOGImage != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Settings.LLMsTxtEnabled { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Settings.RSSFeedURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.Settings.GoogleAnalyticsID != "" && (data.Settings.GoogleAnalyticsEnabled || !data.Settings.GoogleAnalyticsExplicitlySet) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = googleAnalyticsScript(data.Settings.GoogleAnalyticsID).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = analyticsScript(data.PageviewNonce).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = EngagementScript(data.EngagementConfig).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, style := range data.PluginStyles { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ThemeCSS != "" { + templ_7745c5c3_Err = themeStyle(data.ThemeCSS).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, script := range data.Settings.CustomScripts { + if script.Enabled && script.Placement == "head" { + templ_7745c5c3_Err = templ.Raw(script.Code).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + if data.StructuredData != "" { + templ_7745c5c3_Err = structuredDataScript(data.StructuredData).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// structuredDataScript renders JSON-LD structured data +func structuredDataScript(jsonLD string) templ.Component { + return templ.Raw(``) +} + +// AdminBypassBanner renders a banner when admin is bypassing maintenance/coming_soon mode +// This should be rendered at the very start of the to push down all content +func AdminBypassBanner(settings SiteSettingsData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var53 := templ.GetChildren(ctx) + if templ_7745c5c3_Var53 == nil { + templ_7745c5c3_Var53 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if settings.AdminBypassMode != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if settings.AdminBypassMode == "maintenance" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "Maintenance Mode Active — You're seeing the normal site because you're logged in as admin. Manage") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if settings.AdminBypassMode == "coming_soon" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "Coming Soon Mode Active — You're seeing the normal site because you're logged in as admin. Manage") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +// BodyEnd renders custom scripts and admin toolbar before +func BodyEnd(settings SiteSettingsData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var54 := templ.GetChildren(ctx) + if templ_7745c5c3_Var54 == nil { + templ_7745c5c3_Var54 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = recordValidationCall(ctx, "BodyEnd").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, script := range settings.CustomScripts { + if script.Enabled && script.Placement == "body" { + templ_7745c5c3_Err = templ.Raw(script.Code).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = AdminEditorToolbar(settings.Toolbar).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var54.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// themeStyle renders the theme CSS variables +func themeStyleComponent(css string) templ.Component { + return templ.Raw(``) +} + +func themeStyle(css string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var55 := templ.GetChildren(ctx) + if templ_7745c5c3_Var55 == nil { + templ_7745c5c3_Var55 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = themeStyleComponent(css).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// googleAnalyticsScript renders the GA4 inline script +func googleAnalyticsScript(gaID string) templ.Component { + return templ.Raw(``) +} + +// analyticsScript renders the built-in analytics tracking script (cookie-less) +// nonce is used for deduplication with server-side tracking +func analyticsScript(nonce string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var56 := templ.GetChildren(ctx) + if templ_7745c5c3_Var56 == nil { + templ_7745c5c3_Var56 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func themeInitScript(themeMode string) templ.Component { + switch themeMode { + case "light", "dark", "system": + default: + themeMode = "light" + } + // Precedence: admin toolbar override (per-tab, sessionStorage) → visitor + // preference (bn-theme cookie/localStorage) → site default → system. + // Exposed as window.bnApplyTheme so the toolbar theme tester can re-apply + // after changing the override without duplicating this resolution. + return templ.Raw(``) +} + +var _ = templruntime.GeneratedTemplate diff --git a/templates/bn/toolbar.templ b/templates/bn/toolbar.templ new file mode 100644 index 0000000..05d32aa --- /dev/null +++ b/templates/bn/toolbar.templ @@ -0,0 +1,686 @@ +package bn + +import ( + "fmt" + "time" + + "github.com/google/uuid" +) + +// ToolbarData contains data for the admin editor toolbar +type ToolbarData struct { + Enabled bool + PageID uuid.UUID + PageSlug string + PageTitle string + PostType string // "page", "post", "master", "system" + Status string // "published", "draft", "scheduled" + HasUnpublishedChanges bool + PreviewMode string // "published" or "draft" + HidePreviewToggle bool + Animate bool // true only on initial full-page render (entrance animation); false on HTMX re-renders + Position string // "tl", "tc", "tr", "bl", "bc", "br" (default: "tr") + ScheduledAt *time.Time + TemplateName string + AuthorName string + LastModified time.Time + EditURL string + SettingsURL string + HistoryURL string + AnalyticsURL string + + // Analytics snapshot + TodayPageviews int64 + PageviewsTrend string // "up", "down", "flat" + TrendPercent int + + // Blog-specific fields + ReadingTime int // minutes + WordCount int + CategoryCount int + AuthorSlug string +} + +// StatusBadgeClass returns the Tailwind classes for the status badge +func (t ToolbarData) StatusBadgeClass() string { + switch t.Status { + case "published": + return "bg-success text-success-foreground" + case "scheduled": + return "bg-info text-info-foreground" + default: + return "bg-warning text-warning-foreground" + } +} + +// StatusLabel returns a human-readable status label +func (t ToolbarData) StatusLabel() string { + switch t.Status { + case "published": + return "Published" + case "scheduled": + return "Scheduled" + default: + return "Draft" + } +} + +// PostTypeLabel returns a human-readable post type label +func (t ToolbarData) PostTypeLabel() string { + switch t.PostType { + case "post": + return "Post" + case "master": + return "Master" + case "system": + return "System" + default: + return "Page" + } +} + +// IsPreviewMode returns true if currently viewing draft/preview +func (t ToolbarData) IsPreviewMode() bool { + return t.PreviewMode == "draft" +} + +// CanPublish returns true if the publish button should be enabled +func (t ToolbarData) CanPublish() bool { + return t.HasUnpublishedChanges +} + +// LastModifiedFormatted returns the last modified time in a readable format +func (t ToolbarData) LastModifiedFormatted() string { + return t.LastModified.Format("Jan 2, 2006 3:04 PM") +} + +// IsTopPosition returns true if toolbar should be at top (for legacy compatibility) +func (t ToolbarData) IsTopPosition() bool { + return t.Position == "tl" || t.Position == "tc" || t.Position == "tr" || t.Position == "" +} + +// IsBottomPosition returns true if toolbar is at bottom +func (t ToolbarData) IsBottomPosition() bool { + return t.Position == "bl" || t.Position == "bc" || t.Position == "br" +} + +// IsLeftPosition returns true if toolbar is on the left side +func (t ToolbarData) IsLeftPosition() bool { + return t.Position == "tl" || t.Position == "bl" +} + +// IsRightPosition returns true if toolbar is on the right side +func (t ToolbarData) IsRightPosition() bool { + return t.Position == "tr" || t.Position == "br" || t.Position == "" +} + +// IsCenterPosition returns true if toolbar is centered +func (t ToolbarData) IsCenterPosition() bool { + return t.Position == "tc" || t.Position == "bc" +} + +// DropdownDirection returns "up" or "down" based on toolbar vertical position +func (t ToolbarData) DropdownDirection() string { + if t.IsBottomPosition() { + return "up" + } + return "down" +} + +// DropdownAlign returns "left", "center", or "right" based on toolbar horizontal position +func (t ToolbarData) DropdownAlign() string { + if t.IsLeftPosition() { + return "left" + } else if t.IsCenterPosition() { + return "center" + } + return "right" +} + +// EnterAnimationClass returns the entrance-animation classes for the initial +// full-page render. It slides the pill in from the edge it rests against (up for +// bottom positions, down for top) then gives a brief pulse. Empty on HTMX +// re-renders (Animate=false) so publish/discard/reposition swaps appear instantly. +func (t ToolbarData) EnterAnimationClass() string { + if !t.Animate { + return "" + } + if t.IsBottomPosition() { + return "bn-toolbar-enter bn-toolbar-enter-up" + } + return "bn-toolbar-enter bn-toolbar-enter-down" +} + +// PositionClasses returns the positioning classes for the floating pill +func (t ToolbarData) PositionClasses() string { + switch t.Position { + case "tl": + return "top-4 left-4" + case "tc": + return "top-4 left-1/2 -translate-x-1/2" + case "bl": + return "bottom-4 left-4" + case "bc": + return "bottom-4 left-1/2 -translate-x-1/2" + case "br": + return "bottom-4 right-4" + default: // "tr" or empty + return "top-4 right-4" + } +} + +// IsBlogPost returns true if this is a blog post +func (t ToolbarData) IsBlogPost() bool { + return t.PostType == "post" +} + +// TrendIcon returns the trend icon for pageviews +func (t ToolbarData) TrendIcon() string { + switch t.PageviewsTrend { + case "up": + return "↑" + case "down": + return "↓" + default: + return "→" + } +} + +// TrendColorClass returns the color class for the trend indicator +func (t ToolbarData) TrendColorClass() string { + switch t.PageviewsTrend { + case "up": + return "text-success" + case "down": + return "text-destructive" + default: + return "text-muted-foreground" + } +} + +// AdminEditorToolbar renders the floating pill toolbar for admins on public pages +// Uses inverted color scheme - dark on light backgrounds, light on dark backgrounds +templ AdminEditorToolbar(data ToolbarData) { + if data.Enabled { +
+ + + + + + + + + +
+ + +
+ if !data.HidePreviewToggle { + +
+ + + } + +
+ + + + + if data.IsBlogPost() && data.ReadingTime > 0 { + + } + + if data.CanPublish() { + + } + +
+ +
+
+
+ + + } +} + +// PageInfoDropdown renders the page info and quick actions dropdown +templ PageInfoDropdown(data ToolbarData) { +
+ + + if data.TodayPageviews > 0 || data.PageviewsTrend != "" { +
+
+
+ + + + { fmt.Sprintf("%d", data.TodayPageviews) } views today +
+ if data.TrendPercent != 0 { + + { data.TrendIcon() } { fmt.Sprintf("%d%%", abs(data.TrendPercent)) } + + } +
+
+ } + + if data.IsBlogPost() { +
+
+ if data.AuthorName != "" { +
+ + + + if data.AuthorSlug != "" { + { data.AuthorName } + } else { + { data.AuthorName } + } +
+ } + if data.ReadingTime > 0 { + { fmt.Sprintf("%d min read", data.ReadingTime) } + } +
+ if data.WordCount > 0 { +
{ fmt.Sprintf("%d words", data.WordCount) }
+ } +
+ } + +
+
+ Template + { data.TemplateName } +
+
+ Modified + { data.LastModifiedFormatted() } +
+
+ + + +
+ + +
+ +
+
Move toolbar
+
+ @positionButton(data.PageID, "tl", data.Position, "Top left") + @positionButton(data.PageID, "tc", data.Position, "Top center") + @positionButton(data.PageID, "tr", data.Position, "Top right") + @positionButton(data.PageID, "bl", data.Position, "Bottom left") + @positionButton(data.PageID, "bc", data.Position, "Bottom center") + @positionButton(data.PageID, "br", data.Position, "Bottom right") +
+
+ + if data.HasUnpublishedChanges { +
+ +
+ } +
+} + +// positionButton renders a position selector button +templ positionButton(pageID uuid.UUID, pos string, currentPos string, label string) { + {{ active := pos == currentPos || (currentPos == "" && pos == "tr") }} + +} + +// abs returns the absolute value of an integer +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +// copyPageURL generates the script to copy page URL +script copyPageURL(slug string) { + const url = window.location.origin + slug; + navigator.clipboard.writeText(url).then(() => { + // Show brief feedback - could enhance with toast later + const btn = event.currentTarget; + const originalText = btn.innerHTML; + btn.innerHTML = ' Copied!'; + setTimeout(() => { btn.innerHTML = originalText; }, 2000); + }); +} + +// togglePreviewMode toggles between preview (draft) and published mode via URL +script togglePreviewMode(isCurrentlyPreview bool) { + const url = new URL(window.location.href); + if (isCurrentlyPreview) { + // Currently previewing, remove preview param + url.searchParams.delete('preview'); + } else { + // Not previewing, add preview param + url.searchParams.set('preview', '1'); + } + window.location.href = url.toString(); +} diff --git a/templates/bn/toolbar_templ.go b/templates/bn/toolbar_templ.go new file mode 100644 index 0000000..04c094a --- /dev/null +++ b/templates/bn/toolbar_templ.go @@ -0,0 +1,996 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1020 +package bn + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "time" + + "github.com/google/uuid" +) + +// ToolbarData contains data for the admin editor toolbar +type ToolbarData struct { + Enabled bool + PageID uuid.UUID + PageSlug string + PageTitle string + PostType string // "page", "post", "master", "system" + Status string // "published", "draft", "scheduled" + HasUnpublishedChanges bool + PreviewMode string // "published" or "draft" + HidePreviewToggle bool + Animate bool // true only on initial full-page render (entrance animation); false on HTMX re-renders + Position string // "tl", "tc", "tr", "bl", "bc", "br" (default: "tr") + ScheduledAt *time.Time + TemplateName string + AuthorName string + LastModified time.Time + EditURL string + SettingsURL string + HistoryURL string + AnalyticsURL string + + // Analytics snapshot + TodayPageviews int64 + PageviewsTrend string // "up", "down", "flat" + TrendPercent int + + // Blog-specific fields + ReadingTime int // minutes + WordCount int + CategoryCount int + AuthorSlug string +} + +// StatusBadgeClass returns the Tailwind classes for the status badge +func (t ToolbarData) StatusBadgeClass() string { + switch t.Status { + case "published": + return "bg-success text-success-foreground" + case "scheduled": + return "bg-info text-info-foreground" + default: + return "bg-warning text-warning-foreground" + } +} + +// StatusLabel returns a human-readable status label +func (t ToolbarData) StatusLabel() string { + switch t.Status { + case "published": + return "Published" + case "scheduled": + return "Scheduled" + default: + return "Draft" + } +} + +// PostTypeLabel returns a human-readable post type label +func (t ToolbarData) PostTypeLabel() string { + switch t.PostType { + case "post": + return "Post" + case "master": + return "Master" + case "system": + return "System" + default: + return "Page" + } +} + +// IsPreviewMode returns true if currently viewing draft/preview +func (t ToolbarData) IsPreviewMode() bool { + return t.PreviewMode == "draft" +} + +// CanPublish returns true if the publish button should be enabled +func (t ToolbarData) CanPublish() bool { + return t.HasUnpublishedChanges +} + +// LastModifiedFormatted returns the last modified time in a readable format +func (t ToolbarData) LastModifiedFormatted() string { + return t.LastModified.Format("Jan 2, 2006 3:04 PM") +} + +// IsTopPosition returns true if toolbar should be at top (for legacy compatibility) +func (t ToolbarData) IsTopPosition() bool { + return t.Position == "tl" || t.Position == "tc" || t.Position == "tr" || t.Position == "" +} + +// IsBottomPosition returns true if toolbar is at bottom +func (t ToolbarData) IsBottomPosition() bool { + return t.Position == "bl" || t.Position == "bc" || t.Position == "br" +} + +// IsLeftPosition returns true if toolbar is on the left side +func (t ToolbarData) IsLeftPosition() bool { + return t.Position == "tl" || t.Position == "bl" +} + +// IsRightPosition returns true if toolbar is on the right side +func (t ToolbarData) IsRightPosition() bool { + return t.Position == "tr" || t.Position == "br" || t.Position == "" +} + +// IsCenterPosition returns true if toolbar is centered +func (t ToolbarData) IsCenterPosition() bool { + return t.Position == "tc" || t.Position == "bc" +} + +// DropdownDirection returns "up" or "down" based on toolbar vertical position +func (t ToolbarData) DropdownDirection() string { + if t.IsBottomPosition() { + return "up" + } + return "down" +} + +// DropdownAlign returns "left", "center", or "right" based on toolbar horizontal position +func (t ToolbarData) DropdownAlign() string { + if t.IsLeftPosition() { + return "left" + } else if t.IsCenterPosition() { + return "center" + } + return "right" +} + +// EnterAnimationClass returns the entrance-animation classes for the initial +// full-page render. It slides the pill in from the edge it rests against (up for +// bottom positions, down for top) then gives a brief pulse. Empty on HTMX +// re-renders (Animate=false) so publish/discard/reposition swaps appear instantly. +func (t ToolbarData) EnterAnimationClass() string { + if !t.Animate { + return "" + } + if t.IsBottomPosition() { + return "bn-toolbar-enter bn-toolbar-enter-up" + } + return "bn-toolbar-enter bn-toolbar-enter-down" +} + +// PositionClasses returns the positioning classes for the floating pill +func (t ToolbarData) PositionClasses() string { + switch t.Position { + case "tl": + return "top-4 left-4" + case "tc": + return "top-4 left-1/2 -translate-x-1/2" + case "bl": + return "bottom-4 left-4" + case "bc": + return "bottom-4 left-1/2 -translate-x-1/2" + case "br": + return "bottom-4 right-4" + default: // "tr" or empty + return "top-4 right-4" + } +} + +// IsBlogPost returns true if this is a blog post +func (t ToolbarData) IsBlogPost() bool { + return t.PostType == "post" +} + +// TrendIcon returns the trend icon for pageviews +func (t ToolbarData) TrendIcon() string { + switch t.PageviewsTrend { + case "up": + return "↑" + case "down": + return "↓" + default: + return "→" + } +} + +// TrendColorClass returns the color class for the trend indicator +func (t ToolbarData) TrendColorClass() string { + switch t.PageviewsTrend { + case "up": + return "text-success" + case "down": + return "text-destructive" + default: + return "text-muted-foreground" + } +} + +// AdminEditorToolbar renders the floating pill toolbar for admins on public pages +// Uses inverted color scheme - dark on light backgrounds, light on dark backgrounds +func AdminEditorToolbar(data ToolbarData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if data.Enabled { + var templ_7745c5c3_Var2 = []any{"bn-toolbar fixed z-[9999] flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-full backdrop-blur-md transition-all duration-300", data.PositionClasses(), data.EnterAnimationClass()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Edit
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 = []any{"w-2 h-2 rounded-full", templ.KV("bg-success", data.Status == "published"), templ.KV("bg-warning", data.Status == "draft"), templ.KV("bg-info", data.Status == "scheduled"), templ.KV("animate-pulse ring-2 ring-warning/50", data.HasUnpublishedChanges)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !data.HidePreviewToggle { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 = []any{"bn-toolbar-hover inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs transition-colors", templ.KV("!bg-info/20 text-info", data.IsPreviewMode()), templ.KV("bn-toolbar-muted", !data.IsPreviewMode())} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, togglePreviewMode(data.IsPreviewMode())) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.IsBlogPost() && data.ReadingTime > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.CanPublish() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +// PageInfoDropdown renders the page info and quick actions dropdown +func PageInfoDropdown(data ToolbarData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var18 := templ.GetChildren(ctx) + if templ_7745c5c3_Var18 == nil { + templ_7745c5c3_Var18 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.TodayPageviews > 0 || data.PageviewsTrend != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TodayPageviews)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 515, Col: 91} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " views today
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.TrendPercent != 0 { + var templ_7745c5c3_Var20 = []any{"text-xs font-medium", data.TrendColorClass()} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(data.TrendIcon()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 519, Col: 25} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d%%", abs(data.TrendPercent))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 519, Col: 73} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.IsBlogPost() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.AuthorName != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.AuthorSlug != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(data.AuthorName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 535, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(data.AuthorName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 537, Col: 50} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if data.ReadingTime > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d min read", data.ReadingTime)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 542, Col: 86} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.WordCount > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var28 string + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d words", data.WordCount)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 546, Col: 84} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
Template ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(data.TemplateName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 554, Col: 48} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
Modified ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastModifiedFormatted()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `toolbar.templ`, Line: 558, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, copyPageURL(data.PageSlug)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
Move toolbar
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = positionButton(data.PageID, "tl", data.Position, "Top left").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = positionButton(data.PageID, "tc", data.Position, "Top center").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = positionButton(data.PageID, "tr", data.Position, "Top right").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = positionButton(data.PageID, "bl", data.Position, "Bottom left").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = positionButton(data.PageID, "bc", data.Position, "Bottom center").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = positionButton(data.PageID, "br", data.Position, "Bottom right").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.HasUnpublishedChanges { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// positionButton renders a position selector button +func positionButton(pageID uuid.UUID, pos string, currentPos string, label string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var37 := templ.GetChildren(ctx) + if templ_7745c5c3_Var37 == nil { + templ_7745c5c3_Var37 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + active := pos == currentPos || (currentPos == "" && pos == "tr") + var templ_7745c5c3_Var38 = []any{"bn-dd-poschip w-8 h-6 rounded border transition-colors flex items-center justify-center", templ.KV("bn-dd-poschip-active", active)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// abs returns the absolute value of an integer +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +// copyPageURL generates the script to copy page URL +func copyPageURL(slug string) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_copyPageURL_044c`, + Function: `function __templ_copyPageURL_044c(slug){const url = window.location.origin + slug; + navigator.clipboard.writeText(url).then(() => { + // Show brief feedback - could enhance with toast later + const btn = event.currentTarget; + const originalText = btn.innerHTML; + btn.innerHTML = ' Copied!'; + setTimeout(() => { btn.innerHTML = originalText; }, 2000); + }); +}`, + Call: templ.SafeScript(`__templ_copyPageURL_044c`, slug), + CallInline: templ.SafeScriptInline(`__templ_copyPageURL_044c`, slug), + } +} + +// togglePreviewMode toggles between preview (draft) and published mode via URL +func togglePreviewMode(isCurrentlyPreview bool) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_togglePreviewMode_114f`, + Function: `function __templ_togglePreviewMode_114f(isCurrentlyPreview){const url = new URL(window.location.href); + if (isCurrentlyPreview) { + // Currently previewing, remove preview param + url.searchParams.delete('preview'); + } else { + // Not previewing, add preview param + url.searchParams.set('preview', '1'); + } + window.location.href = url.toString(); +}`, + Call: templ.SafeScript(`__templ_togglePreviewMode_114f`, isCurrentlyPreview), + CallInline: templ.SafeScriptInline(`__templ_togglePreviewMode_114f`, isCurrentlyPreview), + } +} + +var _ = templruntime.GeneratedTemplate diff --git a/templates/bn/validation.go b/templates/bn/validation.go new file mode 100644 index 0000000..2315eb5 --- /dev/null +++ b/templates/bn/validation.go @@ -0,0 +1,50 @@ +package bn + +import ( + "context" + "io" + + "github.com/a-h/templ" +) + +// validationContextKey is the key used to store validation tracking in context. +type validationContextKey struct{} + +// ValidationTracker tracks which bn functions are called during template rendering. +// Used during template registration to validate templates include required calls. +type ValidationTracker struct { + HeadCalled bool + BodyEndCalled bool +} + +// NewValidationContext creates a context with validation tracking enabled. +// Use GetValidationTracker after rendering to check which functions were called. +func NewValidationContext(ctx context.Context) (context.Context, *ValidationTracker) { + tracker := &ValidationTracker{} + return context.WithValue(ctx, validationContextKey{}, tracker), tracker +} + +// GetValidationTracker retrieves the validation tracker from context, if any. +func GetValidationTracker(ctx context.Context) *ValidationTracker { + if tracker, ok := ctx.Value(validationContextKey{}).(*ValidationTracker); ok { + return tracker + } + return nil +} + +// recordValidationCall records that a bn function was called during rendering. +// Returns an empty component (renders nothing) but has the side effect of tracking the call. +func recordValidationCall(ctx context.Context, fnName string) templ.Component { + if tracker := GetValidationTracker(ctx); tracker != nil { + switch fnName { + case "Head": + tracker.HeadCalled = true + case "BodyEnd": + tracker.BodyEndCalled = true + } + } + // Return empty component - renders nothing + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + return nil + }) +} diff --git a/templates/pongo/base.html b/templates/pongo/base.html new file mode 100644 index 0000000..524d4c6 --- /dev/null +++ b/templates/pongo/base.html @@ -0,0 +1,9 @@ + + +{{ head_html|safe }} + +{{ admin_banner_html|safe }} +{% block body %}{% endblock %} +{{ body_end_html|safe }} + + diff --git a/templates/pongo/context.go b/templates/pongo/context.go new file mode 100644 index 0000000..460cd5a --- /dev/null +++ b/templates/pongo/context.go @@ -0,0 +1,100 @@ +package pongo + +import ( + "context" + "maps" + + "git.dev.alexdunmow.com/block/ninjatpl" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/templates/bn" +) + +// buildPageContext builds a ninjatpl.Context with pre-rendered head/body HTML +// and all page-level variables from the standard doc map. +func (e *Engine) buildPageContext(ctx context.Context, doc map[string]any) ninjatpl.Context { + title := "Untitled" + if t, ok := doc["title"].(string); ok && t != "" { + title = t + } + + slots := make(map[string]string) + if s, ok := doc["slots"].(map[string]string); ok { + slots = s + } + + themeMode := "dark" + if tm, ok := doc["theme_mode"].(string); ok && tm != "" { + themeMode = tm + } + + themeCSS := "" + if tc, ok := doc["theme_css"].(string); ok { + themeCSS = tc + } + + structuredData := "" + if sd, ok := doc["structured_data"].(string); ok { + structuredData = sd + } + + cssHash := "" + if ch, ok := doc["css_hash"].(string); ok { + cssHash = ch + } + + pageviewNonce := "" + if pn, ok := doc["pageview_nonce"].(string); ok { + pageviewNonce = pn + } + + settings := bn.ParseSiteSettings(doc) + pageMeta := bn.ParsePageMeta(doc) + engagementConfig := bn.ParseEngagementConfig(doc) + + headHTML := renderComponent(ctx, bn.Head(bn.HeadData{ + Title: title, + Settings: settings, + PageMeta: pageMeta, + ThemeMode: themeMode, + ThemeCSS: themeCSS, + PluginStyles: e.stylePaths, + StructuredData: structuredData, + CSSHash: cssHash, + PageviewNonce: pageviewNonce, + EngagementConfig: engagementConfig, + })) + bodyEndHTML := renderComponent(ctx, bn.BodyEnd(settings)) + bannerHTML := renderComponent(ctx, bn.AdminBypassBanner(settings)) + + slotsAny := make(map[string]any, len(slots)) + for k, v := range slots { + slotsAny[k] = v + } + + return ninjatpl.Context{ + "head_html": headHTML, + "body_end_html": bodyEndHTML, + "admin_banner_html": bannerHTML, + + "title": title, + "slots": slotsAny, + "theme_mode": themeMode, + "theme_css": themeCSS, + "css_hash": cssHash, + + "site_settings": settings, + "page_meta": pageMeta, + } +} + +// buildBlockContext builds a ninjatpl.Context for block rendering. +// Content fields are available directly; request context is under "ctx". +func buildBlockContext(ctx context.Context, content map[string]any) ninjatpl.Context { + pongoCtx := make(ninjatpl.Context, len(content)+1) + maps.Copy(pongoCtx, content) + if bc := blocks.GetBlockContext(ctx); bc != nil { + pongoCtx["ctx"] = bc.ToMap() + } + return pongoCtx +} diff --git a/templates/pongo/embed.go b/templates/pongo/embed.go new file mode 100644 index 0000000..0067d29 --- /dev/null +++ b/templates/pongo/embed.go @@ -0,0 +1,6 @@ +package pongo + +import "embed" + +//go:embed base.html +var baseFS embed.FS diff --git a/templates/pongo/engine.go b/templates/pongo/engine.go new file mode 100644 index 0000000..06b8332 --- /dev/null +++ b/templates/pongo/engine.go @@ -0,0 +1,169 @@ +package pongo + +import ( + "bytes" + "context" + "io" + "io/fs" + "maps" + + "git.dev.alexdunmow.com/block/ninjatpl" + + "git.dev.alexdunmow.com/block/pluginsdk/blocks" + "git.dev.alexdunmow.com/block/pluginsdk/templates" +) + +// Engine provides first-class pongo2 template support for BlockNinja plugins. +// Plugins create an Engine with their embedded template FS, then call +// MustPageTemplate / MustBlockTemplate to get functions compatible with +// the template and block registries. +type Engine struct { + set *ninjatpl.TemplateSet + stylePaths []string +} + +// NewEngine creates a pongo2 Engine. +// pluginFS should contain the plugin's .html templates. +// stylePaths are CSS URLs included in the page via bn.Head. +func NewEngine(pluginFS fs.FS, stylePaths ...string) *Engine { + loader := &multiLoader{ + loaders: []ninjatpl.TemplateLoader{ + &fsLoader{fsys: pluginFS}, + &fsLoader{fsys: baseFS}, + }, + } + set := ninjatpl.NewSet("plugin", loader) + return &Engine{set: set, stylePaths: stylePaths} +} + +// pongoComponent wraps a pongo2 template execution as an HTMLComponent. +type pongoComponent struct { + tpl *ninjatpl.Template + ctx ninjatpl.Context +} + +func (c *pongoComponent) Render(ctx context.Context, w io.Writer) error { + return c.tpl.ExecuteWriter(c.ctx, w) +} + +// MustPageTemplate parses a pongo2 page template and returns a TemplateFunc. +// Panics on parse error. +// +// Available context variables in page templates: +// +// {{ head_html|safe }} — full element +// {{ body_end_html|safe }} — body-end scripts and admin toolbar +// {{ admin_banner_html|safe }} — maintenance/coming-soon admin banner +// {{ title }} — page title +// {{ slots.header|safe }} — rendered slot HTML +// {{ theme_mode }} — "light", "dark", or "system" +// {{ theme_css }} — raw CSS custom properties +// {{ css_hash }} — cache-busting hash +// {{ site_settings }} — bn.SiteSettingsData struct +// {{ page_meta }} — bn.PageMeta struct +func (e *Engine) MustPageTemplate(name string) templates.TemplateFunc { + tpl := ninjatpl.Must(e.set.FromFile(name)) + return func(ctx context.Context, doc map[string]any) templates.HTMLComponent { + pongoCtx := e.buildPageContext(ctx, doc) + return &pongoComponent{tpl: tpl, ctx: pongoCtx} + } +} + +// MustBlockTemplate parses a pongo2 block template and returns a BlockFunc. +// Panics on parse error. +// +// The content map fields are available directly as template variables. +// Request context is available under {{ ctx.url }}, {{ ctx.isEditor }}, etc. +func (e *Engine) MustBlockTemplate(name string) blocks.BlockFunc { + tpl := ninjatpl.Must(e.set.FromFile(name)) + return func(ctx context.Context, content map[string]any) string { + pongoCtx := buildBlockContext(ctx, content) + out, err := tpl.Execute(pongoCtx) + if err != nil { + return "" + } + return blocks.ProcessIcons(out) + } +} + +// MustBlockTemplateWithDefaults is like MustBlockTemplate but merges default +// values before rendering. Content keys override defaults. +func (e *Engine) MustBlockTemplateWithDefaults(name string, defaults map[string]any) blocks.BlockFunc { + tpl := ninjatpl.Must(e.set.FromFile(name)) + return func(ctx context.Context, content map[string]any) string { + merged := make(map[string]any, len(defaults)+len(content)) + maps.Copy(merged, defaults) + maps.Copy(merged, content) + pongoCtx := buildBlockContext(ctx, merged) + out, err := tpl.Execute(pongoCtx) + if err != nil { + return "" + } + return blocks.ProcessIcons(out) + } +} + +// MustTemplateOverride parses a pongo2 template for use as a block template +// override (e.g., custom heading/text styling). Same as MustBlockTemplate +// but named distinctly for clarity at the call site. +func (e *Engine) MustTemplateOverride(name string) blocks.BlockFunc { + return e.MustBlockTemplate(name) +} + +// MustEmailWrapper parses a pongo2 email template and returns an +// EmailWrapperFunc. Panics on parse error. +// +// Available context variables: +// +// {{ body|safe }} — the email body HTML +// {{ site_name }} — site name +// {{ site_url }} — site URL +// {{ logo_url }} — site logo URL +// {{ unsubscribe_url }} — unsubscribe link (may be empty) +// {{ preview_text }} — email preview/preheader text +// {{ colors.primary }} — primary hex color +// {{ colors.secondary }} — secondary hex color +// {{ colors.background }} — background hex color +// {{ colors.foreground }} — foreground hex color +// {{ colors.border }} — border hex color +// {{ colors.muted }} — muted hex color +// (and all other EmailColors fields as lowercase keys) +func (e *Engine) MustEmailWrapper(name string) templates.EmailWrapperFunc { + tpl := ninjatpl.Must(e.set.FromFile(name)) + return func(body string, ctx templates.EmailContext) string { + pongoCtx := ninjatpl.Context{ + "body": body, + "site_name": ctx.SiteSettings.SiteName, + "site_url": ctx.SiteSettings.SiteURL, + "logo_url": ctx.SiteSettings.LogoURL, + "support_email": ctx.SiteSettings.SupportEmail, + "unsubscribe_url": ctx.UnsubscribeURL, + "preview_text": ctx.PreviewText, + "colors": map[string]any{ + "primary": ctx.Colors.Primary, + "primaryForeground": ctx.Colors.PrimaryForeground, + "secondary": ctx.Colors.Secondary, + "secondaryForeground": ctx.Colors.SecondaryForeground, + "background": ctx.Colors.Background, + "foreground": ctx.Colors.Foreground, + "muted": ctx.Colors.Muted, + "mutedForeground": ctx.Colors.MutedForeground, + "border": ctx.Colors.Border, + "card": ctx.Colors.Card, + "cardForeground": ctx.Colors.CardForeground, + }, + } + out, err := tpl.Execute(pongoCtx) + if err != nil { + return body + } + return out + } +} + +// renderComponent renders any HTMLComponent to a string. +func renderComponent(ctx context.Context, c templates.HTMLComponent) string { + var buf bytes.Buffer + _ = c.Render(ctx, &buf) + return buf.String() +} diff --git a/templates/pongo/loader.go b/templates/pongo/loader.go new file mode 100644 index 0000000..71b8395 --- /dev/null +++ b/templates/pongo/loader.go @@ -0,0 +1,43 @@ +package pongo + +import ( + "fmt" + "io" + "io/fs" + + "git.dev.alexdunmow.com/block/ninjatpl" +) + +// fsLoader adapts an fs.FS to pongo2's TemplateLoader interface. +type fsLoader struct { + fsys fs.FS +} + +func (l *fsLoader) Abs(base, name string) string { + return name +} + +func (l *fsLoader) Get(path string) (io.Reader, error) { + return l.fsys.Open(path) +} + +// multiLoader chains multiple loaders, returning the first hit. +// Plugin templates can {% extends "base.html" %} where base.html +// lives in the core embedded FS rather than the plugin FS. +type multiLoader struct { + loaders []ninjatpl.TemplateLoader +} + +func (l *multiLoader) Abs(base, name string) string { + return name +} + +func (l *multiLoader) Get(path string) (io.Reader, error) { + for _, loader := range l.loaders { + r, err := loader.Get(path) + if err == nil { + return r, nil + } + } + return nil, fmt.Errorf("pongo: template %q not found in any loader", path) +} diff --git a/templates/registry.go b/templates/registry.go new file mode 100644 index 0000000..fad1c55 --- /dev/null +++ b/templates/registry.go @@ -0,0 +1,10 @@ +package templates + +// TemplateRegistry is the interface plugins use to register templates. +// The CMS provides the concrete implementation. +type TemplateRegistry interface { + Register(key string, fn TemplateFunc) error + RegisterSystemTemplate(meta SystemTemplateMeta) + RegisterPageTemplate(systemKey string, meta PageTemplateMeta, fn TemplateFunc) error + RegisterEmailWrapper(systemKey string, fn EmailWrapperFunc) +} diff --git a/templates/types.go b/templates/types.go new file mode 100644 index 0000000..62d2471 --- /dev/null +++ b/templates/types.go @@ -0,0 +1,64 @@ +package templates + +import ( + "context" + "io" +) + +// HTMLComponent is the generic interface for rendered template output. +// Both templ.Component and pongo2 engine output satisfy this interface. +type HTMLComponent interface { + Render(ctx context.Context, w io.Writer) error +} + +// TemplateFunc is the signature for template functions loaded from plugins. +type TemplateFunc func(ctx context.Context, doc map[string]any) HTMLComponent + +// PageTemplateMeta provides metadata about a page template within a system template. +type PageTemplateMeta struct { + Key string + Title string + Description string + Slots []string +} + +// SystemTemplateMeta provides metadata about a system template (theme). +type SystemTemplateMeta struct { + Key string + Title string + Description string +} + +// EmailWrapperFunc wraps body content in a branded email template. +type EmailWrapperFunc func(body string, ctx EmailContext) string + +// EmailContext provides data for email wrapper rendering. +type EmailContext struct { + Colors EmailColors + SiteSettings EmailSiteData + UnsubscribeURL string + PreviewText string +} + +// EmailColors contains theme colors converted to hex for email inlining. +type EmailColors struct { + Primary string + PrimaryForeground string + Secondary string + SecondaryForeground string + Background string + Foreground string + Muted string + MutedForeground string + Border string + Card string + CardForeground string +} + +// EmailSiteData contains site information for email rendering. +type EmailSiteData struct { + SiteName string + LogoURL string + SiteURL string + SupportEmail string +} diff --git a/video/embed.go b/video/embed.go new file mode 100644 index 0000000..2b157f3 --- /dev/null +++ b/video/embed.go @@ -0,0 +1,69 @@ +package video + +import ( + "fmt" + "net/url" + "regexp" + "strings" +) + +// EmbedInfo holds parsed video embed information. +type EmbedInfo struct { + Provider string + VideoID string + URL string +} + +var ( + youtubeRegexps = []*regexp.Regexp{ + regexp.MustCompile(`(?:youtube\.com/watch\?v=|youtu\.be/|youtube\.com/embed/)([a-zA-Z0-9_-]{11})`), + } + vimeoRegexp = regexp.MustCompile(`vimeo\.com/(\d+)`) + loomRegexp = regexp.MustCompile(`loom\.com/share/([a-zA-Z0-9]+)`) +) + +// ParseEmbedURL parses a video URL and returns embed information. +func ParseEmbedURL(rawURL string) (*EmbedInfo, error) { + parsed, err := url.Parse(rawURL) + if err != nil { + return nil, fmt.Errorf("invalid URL: %w", err) + } + + host := strings.ToLower(parsed.Host) + + if strings.Contains(host, "youtube.com") || strings.Contains(host, "youtu.be") { + for _, re := range youtubeRegexps { + if matches := re.FindStringSubmatch(rawURL); len(matches) > 1 { + return &EmbedInfo{Provider: "youtube", VideoID: matches[1], URL: rawURL}, nil + } + } + } + + if strings.Contains(host, "vimeo.com") { + if matches := vimeoRegexp.FindStringSubmatch(rawURL); len(matches) > 1 { + return &EmbedInfo{Provider: "vimeo", VideoID: matches[1], URL: rawURL}, nil + } + } + + if strings.Contains(host, "loom.com") { + if matches := loomRegexp.FindStringSubmatch(rawURL); len(matches) > 1 { + return &EmbedInfo{Provider: "loom", VideoID: matches[1], URL: rawURL}, nil + } + } + + return nil, fmt.Errorf("unsupported video provider for URL: %s", rawURL) +} + +// EmbedIframeURL returns the iframe embed URL for a video provider and ID. +func EmbedIframeURL(provider, videoID string) string { + switch provider { + case "youtube": + return "https://www.youtube.com/embed/" + videoID + case "vimeo": + return "https://player.vimeo.com/video/" + videoID + case "loom": + return "https://www.loom.com/embed/" + videoID + default: + return "" + } +}