package jsmod import ( "context" "fmt" "path/filepath" "runtime" "strings" "sync" ) type contextKey string 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 } // 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) } // IsSafeMode checks if the provided context indicates that the execution is in safe mode. func IsSafeMode(ctx context.Context) bool { if sm, ok := Get(ctx, "SafeMode").(bool); ok { return sm } return false // Default to false if not specified (internal trusted caller) } type Module struct { Exports map[string]any UnsafeList map[string]bool } var ( modules = make(map[string]*Module) mu sync.RWMutex ) // Register registers a Go module with its exported functions and properties. // These modules will be accessible within the JS environment. // unsafeList identifies methods that require elevated permissions (e.g., file writing, shell execution). func Register(name string, exports map[string]any, unsafeList ...string) { mu.Lock() defer mu.Unlock() if modules[name] == nil { modules[name] = &Module{ Exports: make(map[string]any, len(exports)), UnsafeList: make(map[string]bool), } } for k, v := range exports { modules[name].Exports[k] = v } for _, method := range unsafeList { modules[name].UnsafeList[method] = true } } // GetModules returns all registered modules and their unsafe status. func GetModules() map[string]*Module { mu.RLock() defer mu.RUnlock() res := make(map[string]*Module, len(modules)) for name, mod := range modules { res[name] = mod } return res } // 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 }