@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 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-only releases can differ from the SDK patch. Compose import and SDK defaults
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.32.1 depends on `~0.31.0`: npm installation or
78
- update can select the latest templates patch in the 0.31 minor line. Templates
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. Binary downloads require later SDK support.
281
+ executor/README.md.
264
282
 
265
283
  ## Raw WebAPI profiles
266
284
 
267
- SDK 0.12.1 add opt-in raw responses. In `capabilities.webapi.<profile>`,
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; SDK 0.31.5+ AppShell then shows
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
+ }
@@ -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 app.js or app.css.",
554
- suggestion: "Make ui.dir (or the UI build output) contain app.js and app.css.",
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 (Object.keys(capability.permissions).length &&
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
  }