fix(js): 隔离校验低代码函数定义(by AI)
This commit is contained in:
parent
3787accfda
commit
ec6280f7ab
@ -1,5 +1,10 @@
|
|||||||
# CHANGELOG - go/js
|
# 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)
|
## v1.5.10 (2026-08-17)
|
||||||
- **嵌套回调桥接**: 可变参数中的 lower-camel 配置对象现可包含 JavaScript 回调;普通字段继续使用 `cast` 转换,函数字段由 goja 原生导出,支持 API 流式回调。
|
- **嵌套回调桥接**: 可变参数中的 lower-camel 配置对象现可包含 JavaScript 回调;普通字段继续使用 `cast` 转换,函数字段由 goja 原生导出,支持 API 流式回调。
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
- **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`.
|
- **Context Injection**: Automatic `context.Context` propagation from `js.Call`.
|
||||||
- **Versioned Pool**: Thread-safe VM pool with incremental code synchronization and version checking (`CheckVersion`).
|
- **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()`.
|
- **Function Discovery**: List all defined functions via `FuncList()`.
|
||||||
- **Context Interruption**: Safe execution with `context.Context` cancellation support.
|
- **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.
|
- **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
|
### 3. Discover Functions
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
15
TEST.md
15
TEST.md
@ -1,19 +1,23 @@
|
|||||||
# Test Report - go/js
|
# Test Report - go/js
|
||||||
|
|
||||||
|
## v1.5.11 验证
|
||||||
|
- 独立模块解析、全量测试与基准测试均通过。
|
||||||
|
- `TestDefineValidation` 覆盖语法错误、定义阶段异常、非函数结果、定义超时,以及失败定义不改变共享注册表。
|
||||||
|
|
||||||
## v1.5.10 验证
|
## v1.5.10 验证
|
||||||
- 独立模块解析、全量测试与基准测试均通过。
|
- 独立模块解析、全量测试与基准测试均通过。
|
||||||
- `TestBridgeVariadicNestedCallback` 验证可变 options 中 lower-camel 数据与嵌套 JavaScript 回调同时正确转换。
|
- `TestBridgeVariadicNestedCallback` 验证可变 options 中 lower-camel 数据与嵌套 JavaScript 回调同时正确转换。
|
||||||
|
|
||||||
## Performance (Benchmark)
|
## Performance (Benchmark)
|
||||||
Date: 2026-06-28
|
Date: 2026-08-24
|
||||||
OS: darwin
|
OS: darwin
|
||||||
Arch: amd64
|
Arch: arm64
|
||||||
CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
CPU: Apple M3 Max
|
||||||
|
|
||||||
| Benchmark | Iterations | Time/op |
|
| Benchmark | Iterations | Time/op |
|
||||||
|-----------|------------|---------|
|
|-----------|------------|---------|
|
||||||
| BenchmarkCall | 766462 | 1331 ns/op |
|
| BenchmarkCall | 1578303 | 742.8 ns/op |
|
||||||
| BenchmarkSync | 31066 | 52789 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).*
|
*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] Context and Logger injection.
|
||||||
- [x] Concurrent execution and script versioning.
|
- [x] Concurrent execution and script versioning.
|
||||||
- [x] Script version checking (`CheckVersion`).
|
- [x] Script version checking (`CheckVersion`).
|
||||||
|
- [x] Transactional definition validation and shared Pool fault isolation.
|
||||||
- [x] Function discovery (`FuncList`).
|
- [x] Function discovery (`FuncList`).
|
||||||
- [x] Context cancellation interruption.
|
- [x] Context cancellation interruption.
|
||||||
- [x] Graceful shutdown.
|
- [x] Graceful shutdown.
|
||||||
|
|||||||
16
go.mod
16
go.mod
@ -5,18 +5,18 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.5.5
|
apigo.cc/go/cast v1.5.5
|
||||||
apigo.cc/go/jsmod v1.5.3
|
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
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/config v1.5.3 // indirect
|
apigo.cc/go/config v1.5.4 // indirect
|
||||||
apigo.cc/go/encoding v1.5.5 // indirect
|
apigo.cc/go/encoding v1.5.6 // indirect
|
||||||
apigo.cc/go/file v1.5.5 // indirect
|
apigo.cc/go/file v1.5.6 // indirect
|
||||||
apigo.cc/go/id v1.5.6 // indirect
|
apigo.cc/go/id v1.5.7 // indirect
|
||||||
apigo.cc/go/rand v1.5.3 // indirect
|
apigo.cc/go/rand v1.5.4 // indirect
|
||||||
apigo.cc/go/safe v1.5.2 // indirect
|
apigo.cc/go/safe v1.5.3 // indirect
|
||||||
apigo.cc/go/shell v1.5.4 // indirect
|
apigo.cc/go/shell v1.5.6 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||||
|
|||||||
36
go.sum
36
go.sum
@ -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 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw=
|
||||||
apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
|
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.4 h1:1c/OarGwbz3+6ikurE+a6LJLjtzXvGvbxw3HV/Nk54M=
|
||||||
apigo.cc/go/config v1.5.3/go.mod h1:ZiOAjWa1mQIzszaJZN+kO6YU4GXreng+NxkcK/TAkqQ=
|
apigo.cc/go/config v1.5.4/go.mod h1:oN+D2F8ETIyqKp+Yu8R4PRQlUoqR44o35jAHwLrrAq0=
|
||||||
apigo.cc/go/encoding v1.5.5 h1:kduNLWQgtcQqHYobOuu1djbgg8LedkGOe8f18ZMfqzs=
|
apigo.cc/go/encoding v1.5.6 h1:v02swVfbFGidD4QcX2ktuHHbCjdSbOB85fhzAXay+7M=
|
||||||
apigo.cc/go/encoding v1.5.5/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8=
|
apigo.cc/go/encoding v1.5.6/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.6 h1:Y7w3Tyu4e16VuED7rF2pzba+dzGE+hjDnBlIPVHIfzA=
|
||||||
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
|
apigo.cc/go/file v1.5.6/go.mod h1:9sdW4ylSOA0HWc8Yt8qdnmMf6nn5SUEmjoPKyXpYXIQ=
|
||||||
apigo.cc/go/id v1.5.6 h1:Z3PPp8H8FgNHMwHerxtT2GgstueDv4aCDnUUCoQZoFQ=
|
apigo.cc/go/id v1.5.7 h1:Y5Sx6sQBCAdYMCQPTjODZyGMMd1+WRWCy2dWHVq11XQ=
|
||||||
apigo.cc/go/id v1.5.6/go.mod h1:HcJK691qfBPzvQ/lt8bo/incKUFtG5vENX/rDEgzJh0=
|
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 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
||||||
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
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.11 h1:r7vHkzpdelggNguZZK4e3O9bcaxfQWjXkxsKYw4A8Bo=
|
||||||
apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE=
|
apigo.cc/go/log v1.5.11/go.mod h1:C6qtOn09miyCK7FXEAAwEZASnBow76K7GZFoBaq9eiU=
|
||||||
apigo.cc/go/log v1.5.9 h1:g8JehZrpVJyesesv+JFig5szEELsO4UjShsjlEihCfc=
|
apigo.cc/go/rand v1.5.4 h1:eessFBsKQuoOYdzrStldOGw9f4HqbeO87X95bI4jIBQ=
|
||||||
apigo.cc/go/log v1.5.9/go.mod h1:b4f/UB5Kk7oiFtkXvG4GVilmHGygWgxELRAdEZrIyfs=
|
apigo.cc/go/rand v1.5.4/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
||||||
apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
|
apigo.cc/go/safe v1.5.3 h1:9p/BmdlVWLbekpKByZIFC09Qn8Wdhik2eINiwunBxPs=
|
||||||
apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
apigo.cc/go/safe v1.5.3/go.mod h1:Ay8kEPL76DeXH4ifsVTc/3/sfGHlWLQjAp4vi7GA9AI=
|
||||||
apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
|
apigo.cc/go/shell v1.5.6 h1:2i93lNJ0oy/D5kOmlmOjwVjLPsW7vQBo0trLOF9AAAI=
|
||||||
apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
|
apigo.cc/go/shell v1.5.6/go.mod h1:Bp73DGKESOISWSIGUtL1dsFo5g4SI10GhgsnLJCTpsw=
|
||||||
apigo.cc/go/shell v1.5.4 h1:Kn6lP6I6d9U0hbyUjpKKFdFZ8RPo4vi4V6AYW8YFzrc=
|
|
||||||
apigo.cc/go/shell v1.5.4/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
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/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
|
|||||||
21
pool.go
21
pool.go
@ -127,7 +127,21 @@ func (p *Pool) Define(name string, code string, version int64) error {
|
|||||||
return err
|
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()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
@ -164,8 +178,9 @@ func CheckVersion(name string, version int64) bool {
|
|||||||
|
|
||||||
// parseJSFrame parses a single stack trace line from Goja.
|
// parseJSFrame parses a single stack trace line from Goja.
|
||||||
// Format is:
|
// 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) {
|
func parseJSFrame(line string) (src, lineNum, col string, ok bool) {
|
||||||
if !strings.HasPrefix(line, "\t") {
|
if !strings.HasPrefix(line, "\t") {
|
||||||
return "", "", "", false
|
return "", "", "", false
|
||||||
|
|||||||
29
pool_test.go
29
pool_test.go
@ -161,6 +161,35 @@ func TestDefineValidation(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error for arrow function: %v", err)
|
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) {
|
func TestJSErrorStackTrace(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user