diff --git a/native_backends.go b/native_backends.go index e1d88d1..8b2254c 100644 --- a/native_backends.go +++ b/native_backends.go @@ -48,6 +48,20 @@ func EmitEvent(_ string, _ any) {} // RecordActivity is a no-op outside WASM. func RecordActivity(_, _, _, _ string, _ any) {} +// FetchResponse is also defined in wasm_backends.go (wasip1); provide the +// type here so non-WASM consumers can reference it. +type FetchResponse struct { + Status int `json:"status"` + Body string `json:"body"` + Headers map[string]string `json:"headers"` + Error string `json:"error"` +} + +// Fetch always returns errNotWASM outside a WASM module. +func Fetch(_, _ string, _ map[string]string, _ string) (*FetchResponse, error) { + return nil, 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 7fc01e3..e56df96 100644 --- a/wasm_backends.go +++ b/wasm_backends.go @@ -4,6 +4,7 @@ package plugin import ( "encoding/json" + "fmt" "unsafe" ) @@ -206,6 +207,56 @@ func RecordActivity(taskID, projectID, actorUserID, activityType string, content hostActivityRecord(int64(ptrOf(payloadBytes)), int64(len(payloadBytes))) } +// ── Fetch ───────────────────────────────────────────────────────────────────── + +// FetchResponse is the result of a Fetch call. +type FetchResponse struct { + Status int `json:"status"` + Body string `json:"body"` + Headers map[string]string `json:"headers"` + Error string `json:"error"` +} + +// Fetch makes an outbound HTTP request via the paca.fetch host function. +// The URL's domain must be listed in the plugin manifest's allowedOutboundDomains. +func Fetch(method, rawURL string, headers map[string]string, body string) (*FetchResponse, error) { + req := struct { + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers"` + Body string `json:"body"` + }{ + Method: method, + URL: rawURL, + Headers: headers, + Body: body, + } + reqJSON, err := json.Marshal(req) + if err != nil { + return nil, err + } + outputBuf := make([]byte, 8) + hostFetch( + 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 nil, fmt.Errorf("plugin: fetch: empty response from host") + } + resBytes := append([]byte(nil), wasmSlice(resPtr, resLen)...) + wasmResetAllocator() + var resp FetchResponse + if err := json.Unmarshal(resBytes, &resp); err != nil { + return nil, fmt.Errorf("plugin: fetch: decode response: %w", err) + } + if resp.Error != "" { + return nil, fmt.Errorf("plugin: fetch: %s", resp.Error) + } + return &resp, nil +} + // ── Helpers ─────────────────────────────────────────────────────────────────── //go:nocheckptr diff --git a/wasm_imports.go b/wasm_imports.go index 8393a94..a84a8d4 100644 --- a/wasm_imports.go +++ b/wasm_imports.go @@ -49,6 +49,12 @@ func hostStorageDelete(keyPtr, keyLen int64) int32 //go:noescape func hostEventEmit(topicPtr, topicLen, payloadPtr, payloadLen int64) int32 +// paca.fetch(reqPtr i64, reqLen i64, resPtrPtr i64, resLenPtr i64) +// +//go:wasmimport paca fetch +//go:noescape +func hostFetch(reqPtr, reqLen, resPtrPtr, resLenPtr int64) + // paca.activity_record(payloadPtr i64, payloadLen i64) -> ok i32 // //go:wasmimport paca activity_record