go/pkg/coredeno/denoclient.go
Snider 19e3fd3af7 fix(coredeno): harden security and fix review issues
- Path traversal: CheckPath now requires separator after prefix match
- Store namespace: block reserved '_' prefixed groups
- StoreGet: distinguish ErrNotFound from real DB errors via sentinel
- Store: add rows.Err() checks in GetAll and Render
- gRPC leak: cleanupGRPC on all early-return error paths in OnStartup
- DenoClient: fix fmt.Sprint(nil) → type assertions
- Socket permissions: 0700 dirs, 0600 sockets (owner-only)
- Marketplace: persist SignKey, re-verify manifest on Update
- io/local: resolve symlinks in New() (macOS /var → /private/var)
- Tests: fix sun_path length overflow on macOS

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-19 14:39:56 +00:00

138 lines
3.3 KiB
Go

package coredeno
import (
"bufio"
"encoding/json"
"fmt"
"net"
"sync"
)
// DenoClient communicates with the Deno sidecar's JSON-RPC server over a Unix socket.
// Thread-safe: uses a mutex to serialize requests (one connection, request/response protocol).
type DenoClient struct {
mu sync.Mutex
conn net.Conn
reader *bufio.Reader
}
// DialDeno connects to the Deno JSON-RPC server on the given Unix socket path.
func DialDeno(socketPath string) (*DenoClient, error) {
conn, err := net.Dial("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("deno dial: %w", err)
}
return &DenoClient{
conn: conn,
reader: bufio.NewReader(conn),
}, nil
}
// Close closes the underlying connection.
func (c *DenoClient) Close() error {
return c.conn.Close()
}
func (c *DenoClient) call(req map[string]any) (map[string]any, error) {
c.mu.Lock()
defer c.mu.Unlock()
data, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal: %w", err)
}
data = append(data, '\n')
if _, err := c.conn.Write(data); err != nil {
return nil, fmt.Errorf("write: %w", err)
}
line, err := c.reader.ReadBytes('\n')
if err != nil {
return nil, fmt.Errorf("read: %w", err)
}
var resp map[string]any
if err := json.Unmarshal(line, &resp); err != nil {
return nil, fmt.Errorf("unmarshal: %w", err)
}
if errMsg, ok := resp["error"].(string); ok && errMsg != "" {
return nil, fmt.Errorf("deno: %s", errMsg)
}
return resp, nil
}
// ModulePermissions declares per-module permission scopes for Deno Worker sandboxing.
type ModulePermissions struct {
Read []string `json:"read,omitempty"`
Write []string `json:"write,omitempty"`
Net []string `json:"net,omitempty"`
Run []string `json:"run,omitempty"`
}
// LoadModuleResponse holds the result of a LoadModule call.
type LoadModuleResponse struct {
Ok bool
Error string
}
// LoadModule tells Deno to load a module with the given permissions.
func (c *DenoClient) LoadModule(code, entryPoint string, perms ModulePermissions) (*LoadModuleResponse, error) {
resp, err := c.call(map[string]any{
"method": "LoadModule",
"code": code,
"entry_point": entryPoint,
"permissions": perms,
})
if err != nil {
return nil, err
}
errStr, _ := resp["error"].(string)
return &LoadModuleResponse{
Ok: resp["ok"] == true,
Error: errStr,
}, nil
}
// UnloadModuleResponse holds the result of an UnloadModule call.
type UnloadModuleResponse struct {
Ok bool
}
// UnloadModule tells Deno to unload a module.
func (c *DenoClient) UnloadModule(code string) (*UnloadModuleResponse, error) {
resp, err := c.call(map[string]any{
"method": "UnloadModule",
"code": code,
})
if err != nil {
return nil, err
}
return &UnloadModuleResponse{
Ok: resp["ok"] == true,
}, nil
}
// ModuleStatusResponse holds the result of a ModuleStatus call.
type ModuleStatusResponse struct {
Code string
Status string
}
// ModuleStatus queries the status of a module in the Deno runtime.
func (c *DenoClient) ModuleStatus(code string) (*ModuleStatusResponse, error) {
resp, err := c.call(map[string]any{
"method": "ModuleStatus",
"code": code,
})
if err != nil {
return nil, err
}
respCode, _ := resp["code"].(string)
sts, _ := resp["status"].(string)
return &ModuleStatusResponse{
Code: respCode,
Status: sts,
}, nil
}