2026-05-30 14:11:56 +08:00
|
|
|
package jsmod
|
|
|
|
|
|
2026-05-30 19:26:01 +08:00
|
|
|
import (
|
|
|
|
|
"context"
|
2026-06-21 10:10:15 +08:00
|
|
|
"fmt"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
2026-05-30 19:26:01 +08:00
|
|
|
"sync"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type contextKey string
|
|
|
|
|
|
2026-06-10 09:45:10 +08:00
|
|
|
const internalContextKey contextKey = "__GoJSContext__"
|
|
|
|
|
|
|
|
|
|
// Get retrieves an injected value from the context using the unified context map.
|
|
|
|
|
func Get(ctx context.Context, key string) any {
|
|
|
|
|
if ctx == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if m, ok := ctx.Value(internalContextKey).(map[string]any); ok {
|
|
|
|
|
return m[key]
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-30 19:26:01 +08:00
|
|
|
|
2026-06-10 10:09:55 +08:00
|
|
|
// NewContext returns a new context with the provided injection map.
|
|
|
|
|
func NewContext(parent context.Context, injects map[string]any) context.Context {
|
|
|
|
|
if parent == nil {
|
|
|
|
|
parent = context.Background()
|
|
|
|
|
}
|
|
|
|
|
return context.WithValue(parent, internalContextKey, injects)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 19:26:01 +08:00
|
|
|
// IsSafeMode checks if the provided context indicates that the execution is in safe mode.
|
|
|
|
|
func IsSafeMode(ctx context.Context) bool {
|
2026-06-10 09:45:10 +08:00
|
|
|
if sm, ok := Get(ctx, "SafeMode").(bool); ok {
|
2026-05-30 19:26:01 +08:00
|
|
|
return sm
|
|
|
|
|
}
|
|
|
|
|
return false // Default to false if not specified (internal trusted caller)
|
|
|
|
|
}
|
2026-05-30 14:11:56 +08:00
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
type Module struct {
|
|
|
|
|
Exports map[string]any
|
|
|
|
|
UnsafeList map[string]bool
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 14:11:56 +08:00
|
|
|
var (
|
2026-05-30 15:33:57 +08:00
|
|
|
modules = make(map[string]*Module)
|
2026-05-30 14:11:56 +08:00
|
|
|
mu sync.RWMutex
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Register registers a Go module with its exported functions and properties.
|
|
|
|
|
// These modules will be accessible within the JS environment.
|
2026-05-30 15:33:57 +08:00
|
|
|
// unsafeList identifies methods that require elevated permissions (e.g., file writing, shell execution).
|
|
|
|
|
func Register(name string, exports map[string]any, unsafeList ...string) {
|
2026-05-30 14:11:56 +08:00
|
|
|
mu.Lock()
|
|
|
|
|
defer mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if modules[name] == nil {
|
2026-05-30 15:33:57 +08:00
|
|
|
modules[name] = &Module{
|
|
|
|
|
Exports: make(map[string]any, len(exports)),
|
|
|
|
|
UnsafeList: make(map[string]bool),
|
|
|
|
|
}
|
2026-05-30 14:11:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for k, v := range exports {
|
2026-05-30 15:33:57 +08:00
|
|
|
modules[name].Exports[k] = v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, method := range unsafeList {
|
|
|
|
|
modules[name].UnsafeList[method] = true
|
2026-05-30 14:11:56 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
// GetModules returns all registered modules and their unsafe status.
|
|
|
|
|
func GetModules() map[string]*Module {
|
2026-05-30 14:11:56 +08:00
|
|
|
mu.RLock()
|
|
|
|
|
defer mu.RUnlock()
|
|
|
|
|
|
2026-05-30 15:33:57 +08:00
|
|
|
res := make(map[string]*Module, len(modules))
|
|
|
|
|
for name, mod := range modules {
|
|
|
|
|
res[name] = mod
|
2026-05-30 14:11:56 +08:00
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
2026-06-21 10:10:15 +08:00
|
|
|
|
|
|
|
|
// Error wraps a Go error with dynamic caller stack frames.
|
|
|
|
|
type Error struct {
|
|
|
|
|
Message string
|
|
|
|
|
CallStacks []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *Error) Error() string {
|
|
|
|
|
return e.Message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *Error) Stack() string {
|
|
|
|
|
return strings.Join(e.CallStacks, "\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MakeError wraps an existing error into a *jsmod.Error with the captured Go caller stack.
|
|
|
|
|
func MakeError(err error) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if je, ok := err.(*Error); ok {
|
|
|
|
|
return je
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var callStacks []string
|
|
|
|
|
pcs := make([]uintptr, 32)
|
|
|
|
|
n := runtime.Callers(1, pcs) // skip runtime.Callers, start recording from the caller of MakeError
|
|
|
|
|
frames := runtime.CallersFrames(pcs[:n])
|
|
|
|
|
for {
|
|
|
|
|
frame, more := frames.Next()
|
|
|
|
|
if frame.Function == "" {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
// Skip runtime and bridge internals if they creep in
|
|
|
|
|
if isNoiseFrame(frame.File, frame.Function) {
|
|
|
|
|
if !more {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
file := trimGoPath(frame.File)
|
|
|
|
|
callStacks = append(callStacks, fmt.Sprintf("%s at %s:%d", frame.Function, file, frame.Line))
|
|
|
|
|
if !more {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &Error{
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
CallStacks: callStacks,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func trimGoPath(fullPath string) string {
|
|
|
|
|
dir, file := filepath.Split(fullPath)
|
|
|
|
|
if dir == "" {
|
|
|
|
|
return file
|
|
|
|
|
}
|
|
|
|
|
parent := filepath.Base(filepath.Clean(dir))
|
|
|
|
|
if parent == "." || parent == "/" {
|
|
|
|
|
return file
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(parent, file)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isNoiseFrame(file, function string) bool {
|
|
|
|
|
// Noise paths to skip
|
|
|
|
|
noisePaths := []string{
|
|
|
|
|
"/jsmod/jsmod.go",
|
|
|
|
|
"/js/bridge.go",
|
|
|
|
|
"/js/pool.go",
|
|
|
|
|
"/goja@",
|
|
|
|
|
"/goja/",
|
|
|
|
|
"/src/runtime/",
|
|
|
|
|
"/src/reflect/",
|
|
|
|
|
"/testing/testing.go",
|
|
|
|
|
}
|
|
|
|
|
for _, p := range noisePaths {
|
|
|
|
|
if strings.Contains(file, p) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Noise functions to skip
|
|
|
|
|
noiseFuncs := []string{
|
|
|
|
|
"github.com/dop251/goja",
|
|
|
|
|
"apigo.cc/go/js.wrapGoFunc",
|
|
|
|
|
"apigo.cc/go/js.(*Pool)",
|
|
|
|
|
"reflect.Value",
|
|
|
|
|
"reflect.Type",
|
|
|
|
|
}
|
|
|
|
|
for _, f := range noiseFuncs {
|
|
|
|
|
if strings.Contains(function, f) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|