fix(js): 隔离校验低代码函数定义(by AI)

This commit is contained in:
Star 2026-08-24 12:24:15 +08:00
parent 3787accfda
commit ec6280f7ab
7 changed files with 89 additions and 36 deletions

View File

@ -1,5 +1,10 @@
# CHANGELOG - go/js
## v1.5.11 (2026-08-24)
- **定义隔离**: `Define` 在发布到共享 VM Pool 前编译并在一次性隔离 VM 中验证脚本,拒绝语法错误、定义阶段异常、非函数结果和超过 1 秒的定义执行。
- **故障边界**: 无效定义不再增加 Pool 版本或进入共享脚本列表,避免单个低代码脚本阻塞其他 VM 和服务。
- **依赖对齐**: 更新 `log``config``encoding``file``id``rand``safe``shell` 至 monorepo 当前版本。
## v1.5.10 (2026-08-17)
- **嵌套回调桥接**: 可变参数中的 lower-camel 配置对象现可包含 JavaScript 回调;普通字段继续使用 `cast` 转换,函数字段由 goja 原生导出,支持 API 流式回调。

View File

@ -9,6 +9,7 @@ A lightweight, frictionless, and AI-friendly JavaScript engine for Go applicatio
- **Host Object Fidelity**: Go pointers and structs are preserved when passed back and forth between Go and JS.
- **Context Injection**: Automatic `context.Context` propagation from `js.Call`.
- **Versioned Pool**: Thread-safe VM pool with incremental code synchronization and version checking (`CheckVersion`).
- **Transactional Definitions**: `Define` validates each function in an isolated VM before publishing it, so an invalid low-code script cannot poison the shared VM pool.
- **Function Discovery**: List all defined functions via `FuncList()`.
- **Context Interruption**: Safe execution with `context.Context` cancellation support.
- **Nested Callback Bridging**: JavaScript callbacks inside lower-camel option objects are preserved when converted to Go structs, including variadic streaming options.
@ -45,6 +46,8 @@ func main() {
}
```
`Define` accepts an anonymous function expression. Before registration it rejects syntax errors, definition-time exceptions, non-function results, and definitions that do not finish within one second. Rejected definitions do not change the Pool version or become visible to any runtime VM.
### 3. Discover Functions
```go

15
TEST.md
View File

@ -1,19 +1,23 @@
# Test Report - go/js
## v1.5.11 验证
- 独立模块解析、全量测试与基准测试均通过。
- `TestDefineValidation` 覆盖语法错误、定义阶段异常、非函数结果、定义超时,以及失败定义不改变共享注册表。
## v1.5.10 验证
- 独立模块解析、全量测试与基准测试均通过。
- `TestBridgeVariadicNestedCallback` 验证可变 options 中 lower-camel 数据与嵌套 JavaScript 回调同时正确转换。
## Performance (Benchmark)
Date: 2026-06-28
Date: 2026-08-24
OS: darwin
Arch: amd64
CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
Arch: arm64
CPU: Apple M3 Max
| Benchmark | Iterations | Time/op |
|-----------|------------|---------|
| BenchmarkCall | 766462 | 1331 ns/op |
| BenchmarkSync | 31066 | 52789 ns/op |
| BenchmarkCall | 1578303 | 742.8 ns/op |
| BenchmarkSync | 13456 | 136636 ns/op |
*Note: BenchmarkCall covers the hot path of executing a JS function from the pool. BenchmarkSync covers the cost of defining new code (including VM sync).*
@ -66,6 +70,7 @@ ok apigo.cc/go/js 0.800s
- [x] Context and Logger injection.
- [x] Concurrent execution and script versioning.
- [x] Script version checking (`CheckVersion`).
- [x] Transactional definition validation and shared Pool fault isolation.
- [x] Function discovery (`FuncList`).
- [x] Context cancellation interruption.
- [x] Graceful shutdown.

16
go.mod
View File

@ -5,18 +5,18 @@ go 1.25.0
require (
apigo.cc/go/cast v1.5.5
apigo.cc/go/jsmod v1.5.3
apigo.cc/go/log v1.5.9
apigo.cc/go/log v1.5.11
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
)
require (
apigo.cc/go/config v1.5.3 // indirect
apigo.cc/go/encoding v1.5.5 // indirect
apigo.cc/go/file v1.5.5 // indirect
apigo.cc/go/id v1.5.6 // indirect
apigo.cc/go/rand v1.5.3 // indirect
apigo.cc/go/safe v1.5.2 // indirect
apigo.cc/go/shell v1.5.4 // indirect
apigo.cc/go/config v1.5.4 // indirect
apigo.cc/go/encoding v1.5.6 // indirect
apigo.cc/go/file v1.5.6 // indirect
apigo.cc/go/id v1.5.7 // indirect
apigo.cc/go/rand v1.5.4 // indirect
apigo.cc/go/safe v1.5.3 // indirect
apigo.cc/go/shell v1.5.6 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect

36
go.sum
View File

@ -1,27 +1,23 @@
apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw=
apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
apigo.cc/go/cast v1.5.5 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw=
apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
apigo.cc/go/config v1.5.3 h1:peq1FM2xO+vzPHJf8Dwg3DXm8PtFQMfTFKQj6fpoG7A=
apigo.cc/go/config v1.5.3/go.mod h1:ZiOAjWa1mQIzszaJZN+kO6YU4GXreng+NxkcK/TAkqQ=
apigo.cc/go/encoding v1.5.5 h1:kduNLWQgtcQqHYobOuu1djbgg8LedkGOe8f18ZMfqzs=
apigo.cc/go/encoding v1.5.5/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8=
apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs=
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
apigo.cc/go/id v1.5.6 h1:Z3PPp8H8FgNHMwHerxtT2GgstueDv4aCDnUUCoQZoFQ=
apigo.cc/go/id v1.5.6/go.mod h1:HcJK691qfBPzvQ/lt8bo/incKUFtG5vENX/rDEgzJh0=
apigo.cc/go/config v1.5.4 h1:1c/OarGwbz3+6ikurE+a6LJLjtzXvGvbxw3HV/Nk54M=
apigo.cc/go/config v1.5.4/go.mod h1:oN+D2F8ETIyqKp+Yu8R4PRQlUoqR44o35jAHwLrrAq0=
apigo.cc/go/encoding v1.5.6 h1:v02swVfbFGidD4QcX2ktuHHbCjdSbOB85fhzAXay+7M=
apigo.cc/go/encoding v1.5.6/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8=
apigo.cc/go/file v1.5.6 h1:Y7w3Tyu4e16VuED7rF2pzba+dzGE+hjDnBlIPVHIfzA=
apigo.cc/go/file v1.5.6/go.mod h1:9sdW4ylSOA0HWc8Yt8qdnmMf6nn5SUEmjoPKyXpYXIQ=
apigo.cc/go/id v1.5.7 h1:Y5Sx6sQBCAdYMCQPTjODZyGMMd1+WRWCy2dWHVq11XQ=
apigo.cc/go/id v1.5.7/go.mod h1:fugudFBqfVNakfm91zZzuzU0P4PULzo6sylB8hRMqxA=
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0=
apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE=
apigo.cc/go/log v1.5.9 h1:g8JehZrpVJyesesv+JFig5szEELsO4UjShsjlEihCfc=
apigo.cc/go/log v1.5.9/go.mod h1:b4f/UB5Kk7oiFtkXvG4GVilmHGygWgxELRAdEZrIyfs=
apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc=
apigo.cc/go/shell v1.5.4/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
apigo.cc/go/log v1.5.11 h1:r7vHkzpdelggNguZZK4e3O9bcaxfQWjXkxsKYw4A8Bo=
apigo.cc/go/log v1.5.11/go.mod h1:C6qtOn09miyCK7FXEAAwEZASnBow76K7GZFoBaq9eiU=
apigo.cc/go/rand v1.5.4 h1:eessFBsKQuoOYdzrStldOGw9f4HqbeO87X95bI4jIBQ=
apigo.cc/go/rand v1.5.4/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
apigo.cc/go/safe v1.5.3 h1:9p/BmdlVWLbekpKByZIFC09Qn8Wdhik2eINiwunBxPs=
apigo.cc/go/safe v1.5.3/go.mod h1:Ay8kEPL76DeXH4ifsVTc/3/sfGHlWLQjAp4vi7GA9AI=
apigo.cc/go/shell v1.5.6 h1:2i93lNJ0oy/D5kOmlmOjwVjLPsW7vQBo0trLOF9AAAI=
apigo.cc/go/shell v1.5.6/go.mod h1:Bp73DGKESOISWSIGUtL1dsFo5g4SI10GhgsnLJCTpsw=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=

21
pool.go
View File

@ -127,7 +127,21 @@ func (p *Pool) Define(name string, code string, version int64) error {
return err
}
wrapped := fmt.Sprintf("globalThis['%s'] = (%s);", name, code)
wrapped := fmt.Sprintf("globalThis[%q] = (%s);", name, code)
program, err := goja.Compile(name, wrapped, false)
if err != nil {
return fmt.Errorf("js.Define [%s]: invalid JavaScript: %w", name, err)
}
validationVM := goja.New()
validationTimer := time.AfterFunc(time.Second, func() { validationVM.Interrupt("definition validation timed out") })
if _, err = validationVM.RunProgram(program); err != nil {
validationTimer.Stop()
return fmt.Errorf("js.Define [%s]: definition failed: %w", name, err)
}
validationTimer.Stop()
if _, ok := goja.AssertFunction(validationVM.Get(name)); !ok {
return fmt.Errorf("js.Define [%s]: code must evaluate to a function", name)
}
p.mu.Lock()
defer p.mu.Unlock()
@ -164,8 +178,9 @@ func CheckVersion(name string, version int64) bool {
// parseJSFrame parses a single stack trace line from Goja.
// Format is:
// named: \tat funcName (src:line:col) (optionalPC)
// anon: \tat src:line:col (optionalPC)
//
// named: \tat funcName (src:line:col) (optionalPC)
// anon: \tat src:line:col (optionalPC)
func parseJSFrame(line string) (src, lineNum, col string, ok bool) {
if !strings.HasPrefix(line, "\t") {
return "", "", "", false

View File

@ -161,6 +161,35 @@ func TestDefineValidation(t *testing.T) {
if err != nil {
t.Errorf("unexpected error for arrow function: %v", err)
}
// Invalid JavaScript must not enter the shared registry or poison VMs.
version := p.version
err = p.Define("badSyntax", `({ args }) => { return args })`, 0)
if err == nil || !strings.Contains(err.Error(), "invalid JavaScript") {
t.Fatalf("expected syntax validation error, got %v", err)
}
if p.version != version || p.CheckVersion("badSyntax", 0) {
t.Fatal("invalid JavaScript changed the shared script registry")
}
err = p.Define("throwsWhileDefining", `(() => { throw new Error("definition failed") })()`, 0)
if err == nil || !strings.Contains(err.Error(), "definition failed") {
t.Fatalf("expected definition error, got %v", err)
}
err = p.Define("notAFunction", `({ value: 1 })`, 0)
if err == nil || !strings.Contains(err.Error(), "must evaluate to a function") {
t.Fatalf("expected function validation error, got %v", err)
}
err = p.Define("neverFinishes", `(() => { for (;;) {} })()`, 0)
if err == nil || !strings.Contains(err.Error(), "definition validation timed out") {
t.Fatalf("expected definition timeout, got %v", err)
}
if p.version != version || p.CheckVersion("throwsWhileDefining", 0) || p.CheckVersion("notAFunction", 0) || p.CheckVersion("neverFinishes", 0) {
t.Fatal("failed definitions changed the shared script registry")
}
result, callErr := p.Call("good2", 0, nil, 2, 3)
if callErr != nil || cast.To[int64](result) != 5 {
t.Fatalf("valid function failed after rejected script: result=%v error=%v", result, callErr)
}
}
func TestJSErrorStackTrace(t *testing.T) {