@synopackageland/cli 0.33.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +29 -6
- package/assets/go/route-audit/main.go +526 -0
- package/dist/error-registry.js +7 -2
- package/dist/init.js +6 -2
- package/dist/standalone/api-description.js +3 -0
- package/dist/standalone/bridge-runtime.js +2 -0
- package/dist/standalone/build.js +19 -0
- package/dist/standalone/dsm-window.d.ts +3 -3
- package/dist/standalone/dsm-window.js +23 -6
- package/dist/standalone/identity-gateway.d.ts +14 -0
- package/dist/standalone/identity-gateway.js +55 -3
- package/dist/standalone/inspect-spk.js +29 -1
- package/dist/standalone/manifest.js +50 -1
- package/dist/standalone/route-audit.d.ts +12 -0
- package/dist/standalone/route-audit.js +29 -0
- package/dist/standalone/spk.js +15 -20
- package/dist/standalone/ui-assets.d.ts +3 -0
- package/dist/standalone/ui-assets.js +27 -0
- package/package.json +2 -2
- package/skills/SKILL.md +18 -1
- package/skills/reference/create.md +1 -1
- package/skills/reference/files.md +5 -0
- package/skills/reference/identity-gateway.md +20 -0
- package/skills/reference/identity.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
- Audit the target Linux import graphs of configured Go binaries rather than
|
|
6
|
+
recursively scanning unshipped development commands.
|
|
7
|
+
- Accept fixed, permission-declared `api.HandleHTTP` routes and package-private
|
|
8
|
+
`ipc.ServeUnix` handler graphs with SDK 0.33.0. Reject direct listeners and
|
|
9
|
+
unregistered handlers even when an IPC handler is reused.
|
|
10
|
+
- Bind the raw HTTP prefix inventory and package IPC identity into each Go
|
|
11
|
+
binary. Repeat source validation after UI hooks. Retain executor 0.32.0 bytes
|
|
12
|
+
and templates 0.32.0.
|
|
13
|
+
|
|
14
|
+
## 0.34.0 (2026-10-29)
|
|
15
|
+
|
|
16
|
+
- Enforce explicit App entry and reject undeclared App methods, dynamic route
|
|
17
|
+
names and custom HTTP dispatchers during check and build. Every App method
|
|
18
|
+
must appear in `permissions.json`, including administrator-default methods.
|
|
19
|
+
Register HTTP work through fixed SDK route constructors.
|
|
20
|
+
- Validate and bind `identity_gateway.upstream_auth_paths` for client API/share
|
|
21
|
+
paths that use upstream authentication. Other paths retain DSM entry admission;
|
|
22
|
+
SDK RPC methods retain their own permissions. Use SDK 0.32.0.
|
|
23
|
+
- Load `app.module.js` ES modules and lazy chunks from a content-addressed
|
|
24
|
+
snapshot of the entire frontend output. Module entries take precedence over
|
|
25
|
+
classic `app.js`; both require `app.css`. `inspect` verifies the complete tree.
|
|
26
|
+
Reload an open page after an upgrade if an old lazy chunk is unavailable.
|
|
27
|
+
- Use templates 0.32.0 with SDK/scaffold pins at 0.32.0.
|
|
28
|
+
|
|
3
29
|
## 0.33.2 (2026-10-28)
|
|
4
30
|
|
|
5
31
|
- Load the installed app when a DSM window opens after a package upgrade without a page reload; the loader previously reused the app module the previous build left in the page
|
package/README.md
CHANGED
|
@@ -53,6 +53,10 @@ A backend in Python, Node or Rust uses `backend: {runtime: python|node|rust, ...
|
|
|
53
53
|
the SDK's Go runtime, which checks the DSM session and `permissions.json` and starts the App
|
|
54
54
|
from `target/app` as the package account for each request (see `skills/reference/create.md`).
|
|
55
55
|
|
|
56
|
+
[App entry and registration](docs/app-entry.md) explains administrator-only and
|
|
57
|
+
Application Privileges admission, explicit method policies, gateway conflicts and
|
|
58
|
+
the build-bound SDK checks.
|
|
59
|
+
|
|
56
60
|
## SDK and templates
|
|
57
61
|
|
|
58
62
|
The CLI packages, deploys and checks. The app-side code is the SDK
|
|
@@ -70,12 +74,19 @@ alongside npm lockfiles. Do not disable checksum verification for public SDK
|
|
|
70
74
|
packages. Private mirrors can use explicitly configured Go authentication and
|
|
71
75
|
checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
|
|
72
76
|
|
|
73
|
-
CLI
|
|
77
|
+
CLI 0.35.0 uses templates ~0.32.0, whose default SDK pin remains 0.32.0. Compose import and SDK defaults
|
|
74
78
|
read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
|
|
79
|
+
Every App method must be declared in the permissions table. Dynamic names and
|
|
80
|
+
custom HTTP dispatchers are rejected. With SDK 0.33.0, use fixed
|
|
81
|
+
`api.HandleHTTP` prefixes for authorized raw HTTP and `ipc.ServeUnix` for private
|
|
82
|
+
package Unix sockets. The audit follows each shipped Linux binary's import graph;
|
|
83
|
+
unimported development commands are excluded. Use
|
|
84
|
+
`identity_gateway.upstream_auth_paths` for client API paths that use upstream
|
|
85
|
+
authentication. See [App entry and registration](docs/app-entry.md).
|
|
75
86
|
|
|
76
87
|
`init` and `add` read their templates from the locally installed
|
|
77
|
-
`@synopackageland/templates`. CLI 0.
|
|
78
|
-
update can select the latest templates patch in the 0.
|
|
88
|
+
`@synopackageland/templates`. CLI 0.35.0 depends on `~0.32.0`: npm installation or
|
|
89
|
+
update can select the latest templates patch in the 0.32 minor line. Templates
|
|
79
90
|
patches need only a templates release; update the npm installation to receive them.
|
|
80
91
|
The CLI never fetches templates at runtime, so offline use works after installation;
|
|
81
92
|
commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
|
|
@@ -94,6 +105,13 @@ templates checkout and `SYNOPKGLAND_SDK_DIR` to an SDK checkout; SPKs built with
|
|
|
94
105
|
packs the local SDK into its frontend (`npm install --install-links`) without building it: run
|
|
95
106
|
`npm ci && npm run build` in `$SYNOPKGLAND_SDK_DIR/ts` first, and again after SDK changes.
|
|
96
107
|
|
|
108
|
+
Frontend builds may emit `app.module.js` exporting `mount(root, ctx)`, plus
|
|
109
|
+
`app.css` and relative ES module chunks. The CLI packages that output under a
|
|
110
|
+
content-addressed UI directory, so entry, lazy chunks and CSS share one build
|
|
111
|
+
URL. `inspect` verifies that tree against the build inventory. Classic `app.js`
|
|
112
|
+
Apps keep their existing registration contract and paths. See the SDK's
|
|
113
|
+
[frontend bundle guide](https://github.com/synopackageland/syno-pkg-land-sdk/blob/main/docs/frontend-bundles.md).
|
|
114
|
+
|
|
97
115
|
To check the CLI's DSM terminology rules against templates without a new templates release:
|
|
98
116
|
|
|
99
117
|
```sh
|
|
@@ -260,11 +278,11 @@ Use matching backend/frontend SDK pins. The profile participates in the
|
|
|
260
278
|
normal executor approval digest; a profile change needs administrator approval.
|
|
261
279
|
Users grant the named profile separately. The permanent denylist, current-user
|
|
262
280
|
permission checks, bounded JSON and uncertain-outcome rules are in the SDK's
|
|
263
|
-
executor/README.md.
|
|
281
|
+
executor/README.md.
|
|
264
282
|
|
|
265
283
|
## Raw WebAPI profiles
|
|
266
284
|
|
|
267
|
-
|
|
285
|
+
WebAPI profiles support opt-in raw responses. In `capabilities.webapi.<profile>`,
|
|
268
286
|
set `raw_max_bytes: 26214400` for a 25 MiB MIME limit and explicitly allow
|
|
269
287
|
`SYNO.MailClient.Message.get_original` v10. Omitting the limit preserves JSON-only
|
|
270
288
|
profiles. Changing it changes the approved policy and requires fresh user consent.
|
|
@@ -274,6 +292,11 @@ package's private executor-staging folder with size/SHA-256 and immediate cleanu
|
|
|
274
292
|
Do not use Attachment.download or follow X-Accel-Redirect. See the SDK raw response
|
|
275
293
|
contract for the complete transport and CGI validation rules.
|
|
276
294
|
|
|
295
|
+
`webapi.call` request parameters are JSON-only; it has no request upload field.
|
|
296
|
+
Use streaming `files.write` for uploads, up to 100 MiB per file. Reads and URL
|
|
297
|
+
downloads retain their separate 64 MiB limit. Rebuild with SDK 0.32.0 and approve
|
|
298
|
+
the refreshed executor payload.
|
|
299
|
+
|
|
277
300
|
## App launch mode
|
|
278
301
|
|
|
279
302
|
Native Apps declare `ui.launch: dsm-window | new-page` in `dsm-package.yaml`.
|
|
@@ -284,7 +307,7 @@ framework from its build command. `new-page` registers DSM `type: "standalone"`,
|
|
|
284
307
|
so the main menu and Package Center open a full page on the DSM origin.
|
|
285
308
|
`ctx.fullPage`, `ctx.appIcon`, `ctx.uiUpdate`, session/SynoToken, launch parameters and entry
|
|
286
309
|
permissions use the same wrapper in both modes. `ctx.uiUpdate.check()` reports when
|
|
287
|
-
a package upgrade replaced the UI the page loaded;
|
|
310
|
+
a package upgrade replaced the UI the page loaded; AppShell then shows
|
|
288
311
|
a one-line reload notice. The wrapper sets the standalone
|
|
289
312
|
page title, favicon and device-width viewport. It retains ExtJS; `pureEnv` is
|
|
290
313
|
not appropriate, and alternate Portal ports are not needed (`allowAltPort` is omitted).
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
// Command route-audit inspects registrations without running application code.
|
|
2
|
+
package main
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"bytes"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"go/ast"
|
|
9
|
+
"go/parser"
|
|
10
|
+
"go/token"
|
|
11
|
+
"io"
|
|
12
|
+
"os"
|
|
13
|
+
"os/exec"
|
|
14
|
+
"path/filepath"
|
|
15
|
+
"regexp"
|
|
16
|
+
"sort"
|
|
17
|
+
"strconv"
|
|
18
|
+
"strings"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
type input struct {
|
|
22
|
+
Dir string
|
|
23
|
+
Roots []string
|
|
24
|
+
Arches []string
|
|
25
|
+
Permissions map[string]string
|
|
26
|
+
SDK map[string]map[string]string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type result struct {
|
|
30
|
+
Problems []string `json:"problems"`
|
|
31
|
+
EntryGate bool `json:"entryGate"`
|
|
32
|
+
HTTPRoutes []httpRoute `json:"httpRoutes"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type httpRoute struct {
|
|
36
|
+
Prefix string `json:"prefix"`
|
|
37
|
+
Method string `json:"method"`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sdkModule = "github.com/synopackageland/syno-pkg-land-sdk"
|
|
41
|
+
|
|
42
|
+
// Resolve target build constraints without running any application code.
|
|
43
|
+
func shippedFiles(in input) (map[string]string, error) {
|
|
44
|
+
files := map[string]string{}
|
|
45
|
+
if _, err := os.Stat(filepath.Join(in.Dir, "go.mod")); os.IsNotExist(err) {
|
|
46
|
+
matches, err := filepath.Glob(filepath.Join(in.Dir, "*.go"))
|
|
47
|
+
for _, path := range matches {
|
|
48
|
+
if !strings.HasSuffix(path, "_test.go") {
|
|
49
|
+
files[path] = "fixture"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return files, err
|
|
53
|
+
}
|
|
54
|
+
roots, arches := in.Roots, in.Arches
|
|
55
|
+
if len(roots) == 0 {
|
|
56
|
+
roots = []string{"."}
|
|
57
|
+
}
|
|
58
|
+
if len(arches) == 0 {
|
|
59
|
+
arches = []string{"amd64", "arm64", "arm"}
|
|
60
|
+
}
|
|
61
|
+
for _, arch := range arches {
|
|
62
|
+
cmd := exec.Command("go", append([]string{"list", "-e", "-mod=readonly", "-deps", "-json"}, roots...)...)
|
|
63
|
+
cmd.Dir = in.Dir
|
|
64
|
+
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH="+arch, "GOARM=7", "CGO_ENABLED=0", "GOWORK=off", "GOFLAGS=")
|
|
65
|
+
raw, err := cmd.Output()
|
|
66
|
+
if err != nil {
|
|
67
|
+
return nil, fmt.Errorf("cannot resolve shipped Go import graph")
|
|
68
|
+
}
|
|
69
|
+
d := json.NewDecoder(bytes.NewReader(raw))
|
|
70
|
+
for {
|
|
71
|
+
var p struct {
|
|
72
|
+
Dir, ImportPath string
|
|
73
|
+
GoFiles []string
|
|
74
|
+
Standard bool
|
|
75
|
+
DepOnly bool
|
|
76
|
+
Error *struct{ Err string }
|
|
77
|
+
Module *struct {
|
|
78
|
+
Main bool
|
|
79
|
+
Replace *struct{ Dir string }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
err := d.Decode(&p)
|
|
83
|
+
if err == io.EOF {
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
if err != nil {
|
|
87
|
+
return nil, err
|
|
88
|
+
}
|
|
89
|
+
if !p.DepOnly && p.Error != nil {
|
|
90
|
+
return nil, fmt.Errorf("cannot resolve a configured shipped Go root")
|
|
91
|
+
}
|
|
92
|
+
if p.Standard || p.ImportPath == sdkModule || strings.HasPrefix(p.ImportPath, sdkModule+"/") {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if p.Module == nil || !p.Module.Main && p.Module.Replace == nil {
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
for _, name := range p.GoFiles {
|
|
99
|
+
files[filepath.Join(p.Dir, name)] = p.ImportPath
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if len(files) == 0 {
|
|
104
|
+
return nil, fmt.Errorf("shipped Go roots contain no auditable source")
|
|
105
|
+
}
|
|
106
|
+
return files, nil
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func stringLiteral(expr ast.Expr) string {
|
|
110
|
+
if v, ok := expr.(*ast.BasicLit); ok && v.Kind == token.STRING {
|
|
111
|
+
s, _ := strconv.Unquote(v.Value)
|
|
112
|
+
return s
|
|
113
|
+
}
|
|
114
|
+
return ""
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type source struct {
|
|
118
|
+
file *ast.File
|
|
119
|
+
path, pkg string
|
|
120
|
+
imports map[string]string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Follow the handler argument through local declarations and factories. A
|
|
124
|
+
// boundary never exempts an entire directory, file, or any listener primitive.
|
|
125
|
+
func boundaryHandlers(sources []source) map[ast.Node]bool {
|
|
126
|
+
decls := map[string][]ast.Node{}
|
|
127
|
+
owners := map[ast.Node]source{}
|
|
128
|
+
for _, s := range sources {
|
|
129
|
+
ast.Inspect(s.file, func(n ast.Node) bool {
|
|
130
|
+
if n != nil {
|
|
131
|
+
owners[n] = s
|
|
132
|
+
}
|
|
133
|
+
return true
|
|
134
|
+
})
|
|
135
|
+
for _, decl := range s.file.Decls {
|
|
136
|
+
switch d := decl.(type) {
|
|
137
|
+
case *ast.FuncDecl:
|
|
138
|
+
key := s.pkg + "." + d.Name.Name
|
|
139
|
+
if d.Recv != nil {
|
|
140
|
+
t := d.Recv.List[0].Type
|
|
141
|
+
if v, ok := t.(*ast.StarExpr); ok {
|
|
142
|
+
t = v.X
|
|
143
|
+
}
|
|
144
|
+
if v, ok := t.(*ast.Ident); ok {
|
|
145
|
+
key = s.pkg + "." + v.Name
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
decls[key] = append(decls[key], d)
|
|
149
|
+
case *ast.GenDecl:
|
|
150
|
+
for _, spec := range d.Specs {
|
|
151
|
+
switch v := spec.(type) {
|
|
152
|
+
case *ast.TypeSpec:
|
|
153
|
+
decls[s.pkg+"."+v.Name.Name] = append(decls[s.pkg+"."+v.Name.Name], v)
|
|
154
|
+
case *ast.ValueSpec:
|
|
155
|
+
for _, name := range v.Names {
|
|
156
|
+
decls[s.pkg+"."+name.Name] = append(decls[s.pkg+"."+name.Name], v)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
safe, visited := map[ast.Node]bool{}, map[ast.Node]bool{}
|
|
164
|
+
var mark func(ast.Node)
|
|
165
|
+
mark = func(root ast.Node) {
|
|
166
|
+
if root == nil || visited[root] {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
visited[root] = true
|
|
170
|
+
s := owners[root]
|
|
171
|
+
ast.Inspect(root, func(n ast.Node) bool {
|
|
172
|
+
if n == nil {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
safe[n] = true
|
|
176
|
+
key := ""
|
|
177
|
+
switch v := n.(type) {
|
|
178
|
+
case *ast.Ident:
|
|
179
|
+
key = s.pkg + "." + v.Name
|
|
180
|
+
if v.Obj != nil {
|
|
181
|
+
if d, ok := v.Obj.Decl.(ast.Node); ok {
|
|
182
|
+
mark(d)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
case *ast.SelectorExpr:
|
|
186
|
+
if id, ok := v.X.(*ast.Ident); ok {
|
|
187
|
+
key = s.imports[id.Name] + "." + v.Sel.Name
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for _, decl := range decls[key] {
|
|
191
|
+
mark(decl)
|
|
192
|
+
}
|
|
193
|
+
return true
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
for _, s := range sources {
|
|
197
|
+
ast.Inspect(s.file, func(n ast.Node) bool {
|
|
198
|
+
call, ok := n.(*ast.CallExpr)
|
|
199
|
+
if !ok || len(call.Args) != 3 {
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
203
|
+
if !ok {
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
id, ok := sel.X.(*ast.Ident)
|
|
207
|
+
if ok && (s.imports[id.Name] == sdkModule+"/api" && sel.Sel.Name == "HandleHTTP" ||
|
|
208
|
+
s.imports[id.Name] == sdkModule+"/ipc" && sel.Sel.Name == "ServeUnix") {
|
|
209
|
+
mark(call.Args[2])
|
|
210
|
+
}
|
|
211
|
+
return true
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
return safe
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// A handler admitted to a private/raw boundary cannot also be installed as
|
|
218
|
+
// the CGI root. SDK server construction ends this search because it gates
|
|
219
|
+
// its registered routes before invoking their handlers.
|
|
220
|
+
func reusesBoundary(expr ast.Node, owner source, sources []source, safe map[ast.Node]bool, seen map[ast.Node]bool) bool {
|
|
221
|
+
if expr == nil || seen[expr] {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
seen[expr] = true
|
|
225
|
+
imports := owner.imports
|
|
226
|
+
if call, ok := expr.(*ast.CallExpr); ok {
|
|
227
|
+
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
228
|
+
if id, ok := sel.X.(*ast.Ident); ok && imports[id.Name] == sdkModule+"/api" {
|
|
229
|
+
switch sel.Sel.Name {
|
|
230
|
+
case "NewServerWithPermissions":
|
|
231
|
+
return false
|
|
232
|
+
case "WithRequestDeadline", "WithDescription":
|
|
233
|
+
return len(call.Args) > 0 && reusesBoundary(call.Args[0], owner, sources, safe, seen)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
found := false
|
|
239
|
+
ast.Inspect(expr, func(n ast.Node) bool {
|
|
240
|
+
if n == nil || found {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
if n != expr {
|
|
244
|
+
if _, ok := n.(*ast.CallExpr); ok {
|
|
245
|
+
found = reusesBoundary(n, owner, sources, safe, seen)
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
targetPackage, targetName := "", ""
|
|
250
|
+
if id, ok := n.(*ast.Ident); ok {
|
|
251
|
+
if id.Obj != nil {
|
|
252
|
+
if decl, ok := id.Obj.Decl.(ast.Node); ok {
|
|
253
|
+
found = safe[decl] || reusesBoundary(decl, owner, sources, safe, seen)
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
targetPackage, targetName = owner.pkg, id.Name
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if selector, ok := n.(*ast.SelectorExpr); ok {
|
|
260
|
+
if pkg, ok := selector.X.(*ast.Ident); ok {
|
|
261
|
+
targetPackage, targetName = imports[pkg.Name], selector.Sel.Name
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if targetPackage != "" {
|
|
265
|
+
for _, candidate := range sources {
|
|
266
|
+
if candidate.pkg != targetPackage {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
for _, decl := range candidate.file.Decls {
|
|
270
|
+
var target ast.Node
|
|
271
|
+
switch d := decl.(type) {
|
|
272
|
+
case *ast.FuncDecl:
|
|
273
|
+
if d.Recv == nil && d.Name.Name == targetName {
|
|
274
|
+
target = d
|
|
275
|
+
}
|
|
276
|
+
case *ast.GenDecl:
|
|
277
|
+
for _, spec := range d.Specs {
|
|
278
|
+
switch v := spec.(type) {
|
|
279
|
+
case *ast.TypeSpec:
|
|
280
|
+
if v.Name.Name == targetName {
|
|
281
|
+
target = v
|
|
282
|
+
}
|
|
283
|
+
case *ast.ValueSpec:
|
|
284
|
+
for _, name := range v.Names {
|
|
285
|
+
if name.Name == targetName {
|
|
286
|
+
target = v
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if target != nil && (safe[target] || reusesBoundary(target, candidate, sources, safe, seen)) {
|
|
293
|
+
found = true
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return !found
|
|
299
|
+
})
|
|
300
|
+
return found
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func main() {
|
|
304
|
+
var in input
|
|
305
|
+
if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
|
|
306
|
+
fmt.Fprintln(os.Stderr, err)
|
|
307
|
+
os.Exit(1)
|
|
308
|
+
}
|
|
309
|
+
out := result{Problems: []string{}, HTTPRoutes: []httpRoute{}}
|
|
310
|
+
sdk := map[string]bool{}
|
|
311
|
+
for _, group := range in.SDK {
|
|
312
|
+
for method := range group {
|
|
313
|
+
sdk[method] = true
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
set := token.NewFileSet()
|
|
317
|
+
files, err := shippedFiles(in)
|
|
318
|
+
if err != nil {
|
|
319
|
+
fmt.Fprintln(os.Stderr, err)
|
|
320
|
+
os.Exit(1)
|
|
321
|
+
}
|
|
322
|
+
var sources []source
|
|
323
|
+
for path, pkg := range files {
|
|
324
|
+
file, err := parser.ParseFile(set, path, nil, 0)
|
|
325
|
+
if err != nil {
|
|
326
|
+
fmt.Fprintln(os.Stderr, "invalid shipped Go source")
|
|
327
|
+
os.Exit(1)
|
|
328
|
+
}
|
|
329
|
+
imports := map[string]string{}
|
|
330
|
+
for _, spec := range file.Imports {
|
|
331
|
+
module, _ := strconv.Unquote(spec.Path.Value)
|
|
332
|
+
alias := filepath.Base(module)
|
|
333
|
+
if spec.Name != nil {
|
|
334
|
+
alias = spec.Name.Name
|
|
335
|
+
}
|
|
336
|
+
imports[alias] = module
|
|
337
|
+
}
|
|
338
|
+
sources = append(sources, source{file, path, pkg, imports})
|
|
339
|
+
}
|
|
340
|
+
safe := boundaryHandlers(sources)
|
|
341
|
+
for _, src := range sources {
|
|
342
|
+
path, file, imports := src.path, src.file, src.imports
|
|
343
|
+
shown, _ := filepath.Rel(in.Dir, path)
|
|
344
|
+
problem := func(pos token.Pos, message string) {
|
|
345
|
+
out.Problems = append(out.Problems, fmt.Sprintf("%s:%d: %s", filepath.ToSlash(shown), set.Position(pos).Line, message))
|
|
346
|
+
}
|
|
347
|
+
d, err := os.Lstat(path)
|
|
348
|
+
if err != nil || d.Mode()&os.ModeSymlink != 0 {
|
|
349
|
+
problem(token.NoPos, "Go source symlinks cannot be audited")
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
for dir := filepath.Dir(path); dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
|
|
353
|
+
info, err := os.Lstat(dir)
|
|
354
|
+
if err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
355
|
+
problem(token.NoPos, "Go source directories cannot be symlinks")
|
|
356
|
+
break
|
|
357
|
+
}
|
|
358
|
+
if dir == in.Dir {
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for _, spec := range file.Imports {
|
|
363
|
+
if spec.Name != nil && spec.Name.Name == "." {
|
|
364
|
+
problem(spec.Pos(), "dot imports cannot be audited")
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
constants := map[string]string{}
|
|
368
|
+
direct := map[*ast.SelectorExpr]bool{}
|
|
369
|
+
ast.Inspect(file, func(n ast.Node) bool {
|
|
370
|
+
if call, ok := n.(*ast.CallExpr); ok {
|
|
371
|
+
if selector, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
372
|
+
direct[selector] = true
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return true
|
|
376
|
+
})
|
|
377
|
+
for _, decl := range file.Decls {
|
|
378
|
+
if g, ok := decl.(*ast.GenDecl); ok && g.Tok == token.CONST {
|
|
379
|
+
for _, spec := range g.Specs {
|
|
380
|
+
v := spec.(*ast.ValueSpec)
|
|
381
|
+
for i, name := range v.Names {
|
|
382
|
+
if i < len(v.Values) {
|
|
383
|
+
if lit, ok := v.Values[i].(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
|
384
|
+
constants[name.Name], _ = strconv.Unquote(lit.Value)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
ast.Inspect(file, func(n ast.Node) bool {
|
|
392
|
+
if fn, ok := n.(*ast.FuncDecl); ok && fn.Recv != nil && fn.Name.Name == "ServeHTTP" && !safe[n] {
|
|
393
|
+
problem(fn.Pos(), "unregistered ServeHTTP; use api.HandleHTTP or ipc.ServeUnix")
|
|
394
|
+
}
|
|
395
|
+
selector, ok := n.(*ast.SelectorExpr)
|
|
396
|
+
if !ok {
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
399
|
+
if selector.Sel.Name == "ServeHTTP" && !safe[n] {
|
|
400
|
+
problem(selector.Pos(), "unregistered ServeHTTP dispatch")
|
|
401
|
+
}
|
|
402
|
+
pkg, ok := selector.X.(*ast.Ident)
|
|
403
|
+
if !ok {
|
|
404
|
+
return true
|
|
405
|
+
}
|
|
406
|
+
module := imports[pkg.Name]
|
|
407
|
+
name := selector.Sel.Name
|
|
408
|
+
if module == "net" && (strings.HasPrefix(name, "Listen") || name == "FileListener" || name == "FilePacketConn") ||
|
|
409
|
+
module == "crypto/tls" && name == "Listen" ||
|
|
410
|
+
(module == "syscall" || module == "golang.org/x/sys/unix") && (name == "Bind" || name == "Listen" || name == "Socket") {
|
|
411
|
+
problem(selector.Pos(), "custom listener outside SDK boundaries: "+name)
|
|
412
|
+
}
|
|
413
|
+
if module == sdkModule+"/ipc" && name == "ServeUnix" && !direct[selector] {
|
|
414
|
+
problem(selector.Pos(), "IPC constructor aliases cannot be audited")
|
|
415
|
+
}
|
|
416
|
+
if module == "github.com/synopackageland/syno-pkg-land-sdk/idgateway" && (name == "NewFront" || name == "Build" || name == "Run") {
|
|
417
|
+
problem(selector.Pos(), "gateway listeners must use identity_gateway in the manifest; upstream_auth_paths never exempts SDK RPC or custom HTTP handlers")
|
|
418
|
+
}
|
|
419
|
+
if module == "github.com/synopackageland/syno-pkg-land-sdk/api" {
|
|
420
|
+
switch name {
|
|
421
|
+
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP":
|
|
422
|
+
if !direct[selector] {
|
|
423
|
+
problem(selector.Pos(), "route constructor aliases cannot be audited; register methods directly")
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if module == "net/http" {
|
|
428
|
+
switch name {
|
|
429
|
+
case "Handle", "HandleFunc", "Server", "Serve", "ServeTLS", "ListenAndServe", "ListenAndServeTLS":
|
|
430
|
+
problem(selector.Pos(), "HTTP handler outside the SDK registry: "+name)
|
|
431
|
+
case "HandlerFunc", "NewServeMux", "ServeMux", "FileServer", "FileServerFS", "StripPrefix", "TimeoutHandler", "NotFoundHandler", "RedirectHandler":
|
|
432
|
+
if !safe[n] {
|
|
433
|
+
problem(selector.Pos(), "HTTP handler outside the SDK registry: "+name)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if module == "net/http/httputil" && (name == "ReverseProxy" || name == "NewSingleHostReverseProxy") && !safe[n] {
|
|
438
|
+
problem(selector.Pos(), "HTTP proxy outside the SDK registry")
|
|
439
|
+
}
|
|
440
|
+
if module == "net/http/cgi" && name == "Serve" {
|
|
441
|
+
if !direct[selector] {
|
|
442
|
+
problem(selector.Pos(), "CGI constructor aliases cannot be audited")
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return true
|
|
446
|
+
})
|
|
447
|
+
ast.Inspect(file, func(n ast.Node) bool {
|
|
448
|
+
call, ok := n.(*ast.CallExpr)
|
|
449
|
+
if !ok {
|
|
450
|
+
return true
|
|
451
|
+
}
|
|
452
|
+
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
453
|
+
if !ok {
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
456
|
+
pkg, ok := selector.X.(*ast.Ident)
|
|
457
|
+
if !ok {
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
module := imports[pkg.Name]
|
|
461
|
+
if module == "net/http/cgi" && selector.Sel.Name == "Serve" && len(call.Args) > 0 && reusesBoundary(call.Args[0], src, sources, safe, map[ast.Node]bool{}) {
|
|
462
|
+
problem(call.Pos(), "private or raw HTTP handler reused outside its SDK boundary")
|
|
463
|
+
}
|
|
464
|
+
if module == "github.com/synopackageland/syno-pkg-land-sdk/dsm" && selector.Sel.Name == "NewEntryGate" {
|
|
465
|
+
out.EntryGate = true
|
|
466
|
+
}
|
|
467
|
+
if module != "github.com/synopackageland/syno-pkg-land-sdk/api" {
|
|
468
|
+
return true
|
|
469
|
+
}
|
|
470
|
+
switch selector.Sel.Name {
|
|
471
|
+
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP":
|
|
472
|
+
default:
|
|
473
|
+
return true
|
|
474
|
+
}
|
|
475
|
+
method := ""
|
|
476
|
+
index := 0
|
|
477
|
+
if selector.Sel.Name == "HandleHTTP" {
|
|
478
|
+
index = 1
|
|
479
|
+
}
|
|
480
|
+
if len(call.Args) > index {
|
|
481
|
+
switch arg := call.Args[index].(type) {
|
|
482
|
+
case *ast.BasicLit:
|
|
483
|
+
if arg.Kind == token.STRING {
|
|
484
|
+
method, _ = strconv.Unquote(arg.Value)
|
|
485
|
+
}
|
|
486
|
+
case *ast.Ident:
|
|
487
|
+
method = constants[arg.Name]
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if method == "" {
|
|
491
|
+
problem(call.Pos(), "dynamic route name cannot be audited; register literal method names")
|
|
492
|
+
} else if _, declared := in.Permissions[method]; !declared && !sdk[method] {
|
|
493
|
+
problem(call.Pos(), "registered method missing from permissions.json: "+method)
|
|
494
|
+
}
|
|
495
|
+
if selector.Sel.Name == "HandleHTTP" {
|
|
496
|
+
prefix := ""
|
|
497
|
+
if len(call.Args) > 0 {
|
|
498
|
+
prefix = stringLiteral(call.Args[0])
|
|
499
|
+
}
|
|
500
|
+
if !regexp.MustCompile(`^/([A-Za-z0-9_-]+/)+$`).MatchString(prefix) {
|
|
501
|
+
problem(call.Pos(), "HTTP prefix must be a fixed literal full-segment path ending in /")
|
|
502
|
+
}
|
|
503
|
+
for _, reserved := range []string{"/_sso/", "/_spl/", "/api/", "/api.cgi/", "/rpc/", "/webman/", "/cgi/"} {
|
|
504
|
+
if strings.HasPrefix(prefix, reserved) || strings.HasPrefix(reserved, prefix) {
|
|
505
|
+
problem(call.Pos(), "HTTP prefix overlaps a reserved path")
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if sdk[method] || in.Permissions[method] != "authenticated" && in.Permissions[method] != "administrators" {
|
|
509
|
+
problem(call.Pos(), "HTTP routes require an explicit App authenticated or administrators permission")
|
|
510
|
+
}
|
|
511
|
+
for _, other := range out.HTTPRoutes {
|
|
512
|
+
if strings.HasPrefix(prefix, other.Prefix) || strings.HasPrefix(other.Prefix, prefix) {
|
|
513
|
+
problem(call.Pos(), "HTTP prefixes must not overlap")
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
out.HTTPRoutes = append(out.HTTPRoutes, httpRoute{prefix, method})
|
|
517
|
+
}
|
|
518
|
+
return true
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
sort.Strings(out.Problems)
|
|
522
|
+
sort.Slice(out.HTTPRoutes, func(i, j int) bool { return out.HTTPRoutes[i].Prefix < out.HTTPRoutes[j].Prefix })
|
|
523
|
+
if err := json.NewEncoder(os.Stdout).Encode(out); err != nil {
|
|
524
|
+
os.Exit(1)
|
|
525
|
+
}
|
|
526
|
+
}
|
package/dist/error-registry.js
CHANGED
|
@@ -550,8 +550,8 @@ export const ERROR_REGISTRY = {
|
|
|
550
550
|
},
|
|
551
551
|
STANDALONE_UI_ENTRY_MISSING: {
|
|
552
552
|
code: "STANDALONE_UI_ENTRY_MISSING",
|
|
553
|
-
description: "The UI directory is missing
|
|
554
|
-
suggestion: "Make ui.dir
|
|
553
|
+
description: "The UI directory is missing its JavaScript entry or app.css.",
|
|
554
|
+
suggestion: "Make ui.dir contain app.js (classic) or app.module.js (ES module), plus app.css.",
|
|
555
555
|
},
|
|
556
556
|
STANDALONE_UI_SYMLINK: {
|
|
557
557
|
code: "STANDALONE_UI_SYMLINK",
|
|
@@ -828,6 +828,11 @@ export const ERROR_REGISTRY = {
|
|
|
828
828
|
description: "The display name is invalid.",
|
|
829
829
|
suggestion: "Use single-line text without quotes or backslashes.",
|
|
830
830
|
},
|
|
831
|
+
STANDALONE_API_REGISTRY_INVALID: {
|
|
832
|
+
code: "STANDALONE_API_REGISTRY_INVALID",
|
|
833
|
+
description: "An App handler is outside the central permissions registry.",
|
|
834
|
+
suggestion: "Declare every App method in permissions.json and use SDK route constructors; remove custom HTTP dispatch.",
|
|
835
|
+
},
|
|
831
836
|
STANDALONE_API_PERMISSIONS_CHANGED: {
|
|
832
837
|
code: "STANDALONE_API_PERMISSIONS_CHANGED",
|
|
833
838
|
description: "The API policy changed during the build.",
|
package/dist/init.js
CHANGED
|
@@ -240,10 +240,14 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
240
240
|
}
|
|
241
241
|
for (const file of capability.removed)
|
|
242
242
|
rmSync(join(targetDir, file));
|
|
243
|
-
if (
|
|
244
|
-
isNativeTemplate(template)) {
|
|
243
|
+
if (isNativeTemplate(template)) {
|
|
245
244
|
const path = join(targetDir, PERMISSIONS_FILE[template]);
|
|
246
245
|
const policy = JSON.parse(readFileSync(path, "utf8"));
|
|
246
|
+
// Older template releases relied on the administrator default. Materialize
|
|
247
|
+
// their three example routes so the generated registry is explicit.
|
|
248
|
+
for (const method of ["app.status", "greeting.get", "greeting.set"]) {
|
|
249
|
+
policy.methods[method] ??= "administrators";
|
|
250
|
+
}
|
|
247
251
|
Object.assign(policy.methods, capability.permissions);
|
|
248
252
|
writeFileSync(path, JSON.stringify(policy, null, 2) + "\n");
|
|
249
253
|
}
|