Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions custom_host_call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//go:build wasip1

package plugin

import (
"encoding/json"
"fmt"
)

// CustomHostImport is the calling convention every custom `go:wasmimport`
// host function must use to work with [CallHostFunction]:
// (reqPtr, reqLen, resPtrPtr, resLenPtr int64), no return value — the same
// convention Fetch uses. See Fetch's implementation in this package for the
// exact host-side counterpart shape to mirror.
type CustomHostImport func(reqPtr, reqLen, resPtrPtr, resLenPtr int64)

// CallHostFunction is a low-level escape hatch for a plugin that needs a
// host capability beyond what this SDK exposes directly — e.g. a capability
// only relevant to one kind of plugin (like an SMTP-sending mail plugin's
// outbound socket access), which the host implements and gates behind a
// manifest permission the plugin declares for itself, but which doesn't
// belong in this SDK's general-purpose surface.
//
// The plugin author still writes their own `//go:wasmimport` declaration —
// import bindings are static per-symbol and can't be provided generically —
// following the (reqPtr, reqLen, resPtrPtr, resLenPtr int64) convention.
// CallHostFunction then does the JSON marshal/unmarshal and WASM
// linear-memory pointer plumbing this SDK already uses internally for
// Fetch/DB/etc., so plugin authors don't have to hand-roll unsafe pointer
// arithmetic themselves.
//
// request is marshaled to JSON and passed as the call's request payload.
// response, if non-nil, is populated by unmarshaling the call's JSON
// response into it (pass a pointer, as with json.Unmarshal).
func CallHostFunction(hostFn CustomHostImport, request any, response any) error {
reqJSON, err := json.Marshal(request)
if err != nil {
return err
}
outputBuf := make([]byte, 8)
hostFn(
int64(ptrOf(reqJSON)), int64(len(reqJSON)),
int64(ptrOf(outputBuf)), int64(ptrOf(outputBuf[4:])),
)
resPtr := int32(uint32(outputBuf[0]) | uint32(outputBuf[1])<<8 | uint32(outputBuf[2])<<16 | uint32(outputBuf[3])<<24)
resLen := int32(uint32(outputBuf[4]) | uint32(outputBuf[5])<<8 | uint32(outputBuf[6])<<16 | uint32(outputBuf[7])<<24)
if resLen == 0 {
return fmt.Errorf("plugin: custom host call: empty response from host")
}
resBytes := append([]byte(nil), wasmSlice(resPtr, resLen)...)
wasmResetAllocator()
if response != nil {
if err := json.Unmarshal(resBytes, response); err != nil {
return fmt.Errorf("plugin: custom host call: decode response: %w", err)
}
}
return nil
}
18 changes: 18 additions & 0 deletions native_backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ func Fetch(_, _ string, _ map[string]string, _ string) (*FetchResponse, error) {
return nil, errNotWASM
}

// BrandingInfo is also defined in wasm_backends.go (wasip1); provide the
// type here so non-WASM consumers can reference it.
type BrandingInfo struct {
LogoURL string `json:"logo_url"`
BrandName string `json:"brand_name"`
PrimaryColorLight string `json:"primary_color_light"`
PrimaryColorDark string `json:"primary_color_dark"`
}

// GetBranding always returns errNotWASM outside a WASM module.
func GetBranding() (*BrandingInfo, error) { return nil, errNotWASM }

// CustomHostImport mirrors the wasip1 type so non-WASM code can reference it.
type CustomHostImport func(reqPtr, reqLen, resPtrPtr, resLenPtr int64)

// CallHostFunction always returns errNotWASM outside a WASM module.
func CallHostFunction(_ CustomHostImport, _ any, _ any) error { return errNotWASM }

// ptrOf and hostError are used by wasm_backends.go (wasip1 only); provide
// stubs here so the non-WASM build does not need them.
//
Expand Down
41 changes: 41 additions & 0 deletions wasm_backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,47 @@ func Fetch(method, rawURL string, headers map[string]string, body string) (*Fetc
return &resp, nil
}

// ── GetBranding ───────────────────────────────────────────────────────────────

// BrandingInfo mirrors the workspace's public branding (GET /branding).
// Empty fields mean the admin has not customized that value — plugins
// should fall back to their own defaults in that case.
type BrandingInfo struct {
LogoURL string `json:"logo_url"`
BrandName string `json:"brand_name"`
PrimaryColorLight string `json:"primary_color_light"`
PrimaryColorDark string `json:"primary_color_dark"`
}

type brandingResult struct {
BrandingInfo
Error string `json:"error"`
}

// GetBranding reads the workspace's current branding (logo, brand name,
// primary colors) via the paca.settings_get host function. Call this at
// send/render time rather than caching the result, so that an admin's
// branding change is reflected immediately in anything rendered afterward.
func GetBranding() (*BrandingInfo, error) {
outputBuf := make([]byte, 8)
hostSettingsGet(int64(ptrOf(outputBuf)), int64(ptrOf(outputBuf[4:])))
resPtr := int32(uint32(outputBuf[0]) | uint32(outputBuf[1])<<8 | uint32(outputBuf[2])<<16 | uint32(outputBuf[3])<<24)
resLen := int32(uint32(outputBuf[4]) | uint32(outputBuf[5])<<8 | uint32(outputBuf[6])<<16 | uint32(outputBuf[7])<<24)
if resLen == 0 {
return nil, fmt.Errorf("plugin: settings_get: empty response from host")
}
resBytes := append([]byte(nil), wasmSlice(resPtr, resLen)...)
wasmResetAllocator()
var res brandingResult
if err := json.Unmarshal(resBytes, &res); err != nil {
return nil, fmt.Errorf("plugin: settings_get: decode response: %w", err)
}
if res.Error != "" {
return nil, fmt.Errorf("plugin: settings_get: %s", res.Error)
}
return &res.BrandingInfo, nil
}

// ── Helpers ───────────────────────────────────────────────────────────────────

//go:nocheckptr
Expand Down
6 changes: 6 additions & 0 deletions wasm_imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,9 @@ func hostConfigGet(keyPtr, keyLen, valuePtrPtr, valueLenPtr int64)
//go:wasmimport paca permission_check
//go:noescape
func hostPermissionCheck(permissionPtr, permissionLen int64) int32

// paca.settings_get(resPtrPtr i64, resLenPtr i64)
//
//go:wasmimport paca settings_get
//go:noescape
func hostSettingsGet(resPtrPtr, resLenPtr int64)
Loading