mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-17 05:24:13 +00:00
HostFunctions is a process-wide singleton (one per gateway engine). Its `invCtx` field is shared across all WASM instances. For STATELESS execution the executor sets/clears it per-call but the lock is released before WASM runs — two concurrent invocations can race on the field and one's host call can read the other's identity. Window is microseconds. For PERSISTENT WS the bug was much worse: invCtx used to be bound ONCE at instantiation and reused for the connection's lifetime. Two simultaneous persistent WS connections from different namespaces / wallets overwrote each other's invCtx, and EVERY subsequent function_invoke / GetCallerJWTSubject / GetCallerWallet / GetSecret call from inside the WASM read whatever was bound LAST. Result: silent identity leak across tenants for as long as the connections overlapped. Fix: per-call invCtx propagation through Go's context.Context. wazero passes the ctx given to api.Function.Call through to host function callbacks, so every WASM-host hop carries its own invCtx. - pkg/serverless/invocation_context.go (new): WithInvocationContext + InvocationContextFromCtx helpers using an unexported invCtxKey. - pkg/serverless/hostfunctions/invocation_context.go (new): currentInvocationContext(ctx) — ctx-attached invCtx wins over the singleton field. - All host accessors (FunctionInvoke, GetEnv, GetSecret, GetRequestID, GetCallerWallet, GetWSClientID, GetCallerClaim, GetCallerJWTSubject) now route through currentInvocationContext(ctx). - pkg/serverless/persistent/instance.go: every export call's ctx is wrapped with the per-instance invCtx before being passed to wazero. - pkg/gateway/handlers/serverless/ws_persistent_handler.go: invCtx is built per-frame and attached to ctx, not stored on a shared field. - pkg/serverless/engine.go: removed the SetInvocationContext call at InstantiatePersistent (no longer needed; ctx carries it). Stateless still uses the singleton field — its race is latent since the host-functions split and migrating it is a separate scoped change. Tests: - hostfunctions/invocation_context_test.go covers ctx-wins-over-singleton. - gateway/handlers/serverless/ws_persistent_handler_test.go covers the per-frame ctx wiring. - cli/functions/build_test.go is new coverage for the build path touched in this change. VERSION bumped to 0.122.24.
125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
package hostfunctions
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/push"
|
|
"github.com/DeBrosOfficial/network/pkg/serverless"
|
|
)
|
|
|
|
// PushSendArgs is the JSON payload format the WASM caller marshals into
|
|
// the `msgJSON` argument of PushSend. Mirrors push.PushMessage minus the
|
|
// device-token (which is filled in per-device by the dispatcher).
|
|
type PushSendArgs struct {
|
|
Title string `json:"title,omitempty"`
|
|
Body string `json:"body,omitempty"`
|
|
Channel string `json:"channel,omitempty"`
|
|
Priority string `json:"priority,omitempty"` // "high" | "normal" | ""
|
|
Badge int `json:"badge,omitempty"`
|
|
Sound string `json:"sound,omitempty"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
// MaxPushSendArgsBytes caps the JSON arg size to a few KB. Push payloads
|
|
// are small by construction (APNs caps at 4KB, ntfy/Expo similar).
|
|
const MaxPushSendArgsBytes = 16 * 1024
|
|
|
|
// PushSend implements serverless.HostServices.PushSend.
|
|
//
|
|
// Sends a push notification to all devices the user has registered in the
|
|
// function's namespace. The caller can only target users in their own
|
|
// namespace — the dispatcher reads the namespace from the invocation
|
|
// context (set by the engine before invoking).
|
|
//
|
|
// If push is not configured on this gateway (no dispatcher AND no
|
|
// manager), this returns nil (silent no-op) so functions remain portable
|
|
// across environments.
|
|
//
|
|
// When the manager is present (bug #220 follow-up), it routes through
|
|
// per-namespace config — tenants who have set their own ntfy / expo via
|
|
// PUT /v1/push/config get their providers; namespaces with no config
|
|
// fall back to the gateway YAML defaults via the manager's resolution.
|
|
func (h *HostFunctions) PushSend(ctx context.Context, userID string, msgJSON []byte) error {
|
|
if h.pushManager == nil && h.pushDispatcher == nil {
|
|
// Silent no-op — push isn't configured on this gateway.
|
|
return nil
|
|
}
|
|
if userID == "" {
|
|
return &serverless.HostFunctionError{
|
|
Function: "push_send",
|
|
Cause: fmt.Errorf("user_id required"),
|
|
}
|
|
}
|
|
if len(msgJSON) > MaxPushSendArgsBytes {
|
|
return &serverless.HostFunctionError{
|
|
Function: "push_send",
|
|
Cause: fmt.Errorf("msg too large: max %d bytes", MaxPushSendArgsBytes),
|
|
}
|
|
}
|
|
|
|
var args PushSendArgs
|
|
if err := json.Unmarshal(msgJSON, &args); err != nil {
|
|
return &serverless.HostFunctionError{
|
|
Function: "push_send",
|
|
Cause: fmt.Errorf("invalid json: %w", err),
|
|
}
|
|
}
|
|
|
|
// Resolve namespace from the current invocation context. A function
|
|
// can NEVER push to another namespace's users — the namespace is
|
|
// trusted server-side, not from the WASM input.
|
|
// ctx-attached invCtx wins over singleton; see invocation_context.go.
|
|
var namespace string
|
|
if cur := h.currentInvocationContext(ctx); cur != nil {
|
|
namespace = cur.Namespace
|
|
}
|
|
|
|
if namespace == "" {
|
|
return &serverless.HostFunctionError{
|
|
Function: "push_send",
|
|
Cause: fmt.Errorf("no namespace in invocation context"),
|
|
}
|
|
}
|
|
|
|
priority := push.PriorityNormal
|
|
switch args.Priority {
|
|
case "high":
|
|
priority = push.PriorityHigh
|
|
case "normal", "":
|
|
priority = push.PriorityNormal
|
|
}
|
|
|
|
msg := push.PushMessage{
|
|
Title: args.Title,
|
|
Body: args.Body,
|
|
Channel: args.Channel,
|
|
Priority: priority,
|
|
Badge: args.Badge,
|
|
Sound: args.Sound,
|
|
Data: args.Data,
|
|
}
|
|
|
|
// Route through Manager when present so per-namespace push config
|
|
// (set via PUT /v1/push/config) takes effect. The Manager itself
|
|
// falls back to YAML defaults if the namespace hasn't set anything.
|
|
var sendErr error
|
|
if h.pushManager != nil {
|
|
sendErr = h.pushManager.SendToUser(ctx, namespace, userID, msg)
|
|
// ErrPushNotConfigured here would mean: no per-namespace config
|
|
// AND no YAML defaults. Treat as silent no-op for portability —
|
|
// same contract as "no dispatcher at all". Functions can't depend
|
|
// on push being available.
|
|
if sendErr != nil && sendErr.Error() == push.ErrPushNotConfigured.Error() {
|
|
return nil
|
|
}
|
|
} else {
|
|
sendErr = h.pushDispatcher.SendToUser(ctx, namespace, userID, msg)
|
|
}
|
|
if sendErr != nil {
|
|
return &serverless.HostFunctionError{Function: "push_send", Cause: sendErr}
|
|
}
|
|
return nil
|
|
}
|