diff --git a/custom_host_call.go b/custom_host_call.go new file mode 100644 index 0000000..6aa117b --- /dev/null +++ b/custom_host_call.go @@ -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 +} diff --git a/native_backends.go b/native_backends.go index 2cff359..ca4270b 100644 --- a/native_backends.go +++ b/native_backends.go @@ -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. // diff --git a/wasm_backends.go b/wasm_backends.go index ef0c557..28e9b45 100644 --- a/wasm_backends.go +++ b/wasm_backends.go @@ -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 diff --git a/wasm_imports.go b/wasm_imports.go index e1dde44..0d5bfaa 100644 --- a/wasm_imports.go +++ b/wasm_imports.go @@ -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)