Bootstrap the plugin-facing SDK module so the fleet migration (P2) becomes a mechanical block/core/X -> block/pluginsdk/X import rewrite. - abi/proto/v1: the single source-of-truth ABI proto tree, copied from the cms authoring source (cms/backend/abi/proto/v1) with go_package retargeted to git.dev.alexdunmow.com/block/pluginsdk/abi/v1;abiv1. Kills the hand-kept cms/core proto duplication (audit gap 4). - abi/v1: generated Go bindings (buf generate); byte-identical to core's abi/v1 save the embedded go_package path. - plugin/ (registration + DI surface), plugin/wasmguest/** (transport shim, caps stubs, bnwasm db driver, testdata fixtures + golden .pb), and the guest-facing type packages: blocks (+builtin/shared/tags), templates (+pongo/bn), auth, settings, content, gating, crypto, rbac, video, ai, subscriptions, menus, datasources. Internal imports rewritten core -> pluginsdk; zero block/core references remain. - README/AGENTS(+CLAUDE symlink)/Makefile: proto is the contract, Go is one binding; no replace directives; templates/bn is a synced copy authored in cms. Spec: cms docs/superpowers/specs/2026-07-07-proto-first-plugin-sdk-design.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
226 lines
6.9 KiB
Go
226 lines
6.9 KiB
Go
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
|
|
}
|