@synopackageland/cli 0.35.0 → 0.37.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 +54 -0
- package/README.md +15 -3
- package/assets/go/route-audit/main.go +181 -8
- package/dist/build-spk/install-wizard.d.ts +2 -0
- package/dist/build-spk/install-wizard.js +4 -0
- package/dist/build-spk/wizard-messages.d.ts +22 -0
- package/dist/build-spk/wizard-messages.js +22 -0
- package/dist/cli.js +33 -0
- package/dist/error-registry.js +15 -0
- package/dist/skill.js +5 -0
- package/dist/standalone/api-permissions.d.ts +4 -1
- package/dist/standalone/api-permissions.js +10 -4
- package/dist/standalone/build.js +5 -0
- package/dist/standalone/hyper-backup.d.ts +59 -0
- package/dist/standalone/hyper-backup.js +180 -0
- package/dist/standalone/inspect-spk.d.ts +5 -0
- package/dist/standalone/inspect-spk.js +28 -10
- package/dist/standalone/manifest.d.ts +9 -0
- package/dist/standalone/manifest.js +32 -1
- package/dist/standalone/shared-folder.d.ts +6 -1
- package/dist/standalone/shared-folder.js +13 -0
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +20 -6
- package/package.json +2 -2
- package/skills/SKILL.md +34 -2
- package/skills/reference/backup.md +70 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
- Native Apps appear in Hyper Backup as applications by default. Build writes
|
|
6
|
+
`scripts/backup/{version,info,export,import}` from a fixed template and ships
|
|
7
|
+
the SDK backup control as `backup/control` with `backup/config.json`.
|
|
8
|
+
Hyper Backup runs the scripts as root; they only switch to the owner of the
|
|
9
|
+
App's data folder (refusing root), run the control as that package account
|
|
10
|
+
and move one archive they never read. Offline backup is the default
|
|
11
|
+
(Hyper Backup stops and restarts the App); `backup.online`,
|
|
12
|
+
`backup.data_version` and `backup.hook` are optional, and
|
|
13
|
+
`backup: {hyper_backup: false}` opts out. Compose Apps are not covered.
|
|
14
|
+
`check --json` reports `hyperBackup`; `inspect` verifies the exact script
|
|
15
|
+
bytes, modes, control and config, and refuses any other file in
|
|
16
|
+
`scripts/backup/`, such as `can_export` or `info.dynamic`, which Hyper
|
|
17
|
+
Backup would run as root while only listing applications. An SDK without
|
|
18
|
+
the control skips the default with `SDK_APP_BACKUP_UNSUPPORTED`; without Go
|
|
19
|
+
(prebuilt backends) it is skipped with `SDK_APP_BACKUP_BUILD_FAILED`; an
|
|
20
|
+
explicit `backup` fails in both cases. New
|
|
21
|
+
errors: `BACKUP_INVALID`, `BACKUP_COMPOSE_UNSUPPORTED`,
|
|
22
|
+
`BACKUP_HOOK_MISSING`, `SDK_APP_BACKUP_UNSUPPORTED`,
|
|
23
|
+
`SDK_APP_BACKUP_BUILD_FAILED`. New skill reference `backup`.
|
|
24
|
+
- Rebuilt native Apps gain a fixed `scripts/backup/` shim that Hyper Backup
|
|
25
|
+
runs as root. The App itself still runs as the package user, and its
|
|
26
|
+
privilege tier is unchanged. Opt out with `backup: { hyper_backup: false }`.
|
|
27
|
+
- Pin SDK/App 0.35.0.
|
|
28
|
+
|
|
29
|
+
## 0.36.0 (2026-10-29)
|
|
30
|
+
|
|
31
|
+
- Stop treating `net.Listener` and `net.ListenConfig` types as listeners.
|
|
32
|
+
Actual constructors, aliases and declared listener/server methods remain
|
|
33
|
+
rejected. Accept SDK child-port reservation/probe, named private HTTP servers
|
|
34
|
+
and peer-credential executor transport as explicit boundaries.
|
|
35
|
+
- Add `audit-entry` for every custom-hook Go payload's own target import graph.
|
|
36
|
+
It never executes the App. Keep all files of imported shared packages in scope.
|
|
37
|
+
- Bind ordinary CGI/workers to IPC tier `none`; only separately approved root
|
|
38
|
+
helper builds may use executor-tier peer serving. Update the App skill for
|
|
39
|
+
SDK/App 0.34.0, named data planes, child ports and helper boundaries.
|
|
40
|
+
|
|
41
|
+
- Audit `(*api.Server).InvokeAsAdministrator` calls: each must name a literal
|
|
42
|
+
method declared `administrators` in permissions.json. Package commands outside
|
|
43
|
+
CGI use it instead of calling `ServeHTTP` on a server in process, which the
|
|
44
|
+
audit still rejects. Requires the SDK release with `InvokeAsAdministrator`.
|
|
45
|
+
- Accept the `integration` permission for Go `api.HandleIntegration` service
|
|
46
|
+
callbacks. `dsm-package.yaml` `integrations` must list exactly those methods.
|
|
47
|
+
The route audit requires the constructor and permission together, admits only
|
|
48
|
+
the callback's handler graph, and binds each `?method=` selector into the
|
|
49
|
+
HTTP route inventory. The install and upgrade wizards list the callbacks for
|
|
50
|
+
administrator approval; `inspect` verifies them. Requires the SDK release
|
|
51
|
+
with `api.HandleIntegration`.
|
|
52
|
+
- Stop the route audit from following a handler's receiver or parameter to
|
|
53
|
+
its type. A method value such as `cfg.serve` used to admit every method of
|
|
54
|
+
that type, which could hide an unregistered `ServeHTTP` dispatch elsewhere
|
|
55
|
+
on it.
|
|
56
|
+
|
|
3
57
|
## 0.35.0 (2026-10-29)
|
|
4
58
|
|
|
5
59
|
- Audit the target Linux import graphs of configured Go binaries rather than
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ alongside npm lockfiles. Do not disable checksum verification for public SDK
|
|
|
74
74
|
packages. Private mirrors can use explicitly configured Go authentication and
|
|
75
75
|
checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
|
|
76
76
|
|
|
77
|
-
CLI 0.
|
|
77
|
+
CLI 0.37.0 uses templates ~0.33.0, whose default SDK pin is 0.35.0. Compose import and SDK defaults
|
|
78
78
|
read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
|
|
79
79
|
Every App method must be declared in the permissions table. Dynamic names and
|
|
80
80
|
custom HTTP dispatchers are rejected. With SDK 0.33.0, use fixed
|
|
@@ -85,8 +85,8 @@ unimported development commands are excluded. Use
|
|
|
85
85
|
authentication. See [App entry and registration](docs/app-entry.md).
|
|
86
86
|
|
|
87
87
|
`init` and `add` read their templates from the locally installed
|
|
88
|
-
`@synopackageland/templates`. CLI 0.
|
|
89
|
-
update can select the latest templates patch in the 0.
|
|
88
|
+
`@synopackageland/templates`. CLI 0.37.0 depends on `~0.33.0`: npm installation or
|
|
89
|
+
update can select the latest templates patch in the 0.33 minor line. Templates
|
|
90
90
|
patches need only a templates release; update the npm installation to receive them.
|
|
91
91
|
The CLI never fetches templates at runtime, so offline use works after installation;
|
|
92
92
|
commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
|
|
@@ -173,6 +173,18 @@ template changes and executor-only handover eligibility. Without a comparable
|
|
|
173
173
|
baseline, approval retention is unknown. Eligibility still requires live protected
|
|
174
174
|
runtime and exact DSM task readback. See [the approval guide](skills/reference/approval-upgrades.md).
|
|
175
175
|
|
|
176
|
+
## Hyper Backup
|
|
177
|
+
|
|
178
|
+
Native Apps appear in Hyper Backup's backup wizard as applications. By default Hyper Backup stops
|
|
179
|
+
the App, the SDK archives its data folder (the package's `var`) and Hyper Backup starts it again;
|
|
180
|
+
a restore replaces that folder. `backup: {online: true}` keeps the App running and snapshots
|
|
181
|
+
SQLite databases; `data_version` guards restores of newer data; `hook` runs a packaged
|
|
182
|
+
`freeze`/`thaw` executable; `backup: {hyper_backup: false}` opts out. Compose Apps are not
|
|
183
|
+
covered. The build compiles the SDK backup control with Go, also for a prebuilt backend; without
|
|
184
|
+
Go it skips the default backup with a warning. Hyper Backup runs the generated `scripts/backup/` as root: a fixed script that only
|
|
185
|
+
switches to the package account and moves one archive it never reads. This DSM-owned hook does
|
|
186
|
+
not change how the App runs or its privilege tier. See `synopkgland skill show backup`.
|
|
187
|
+
|
|
176
188
|
## Commands
|
|
177
189
|
|
|
178
190
|
| Command | Purpose |
|
|
@@ -178,6 +178,12 @@ func boundaryHandlers(sources []source) map[ast.Node]bool {
|
|
|
178
178
|
case *ast.Ident:
|
|
179
179
|
key = s.pkg + "." + v.Name
|
|
180
180
|
if v.Obj != nil {
|
|
181
|
+
// A parameter or receiver carries a value built elsewhere.
|
|
182
|
+
// Following its type would admit every method of that type,
|
|
183
|
+
// such as an App Config whose method value is the handler.
|
|
184
|
+
if _, param := v.Obj.Decl.(*ast.Field); param {
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
181
187
|
if d, ok := v.Obj.Decl.(ast.Node); ok {
|
|
182
188
|
mark(d)
|
|
183
189
|
}
|
|
@@ -196,6 +202,13 @@ func boundaryHandlers(sources []source) map[ast.Node]bool {
|
|
|
196
202
|
for _, s := range sources {
|
|
197
203
|
ast.Inspect(s.file, func(n ast.Node) bool {
|
|
198
204
|
call, ok := n.(*ast.CallExpr)
|
|
205
|
+
if ok && len(call.Args) == 2 {
|
|
206
|
+
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
207
|
+
if id, ok := sel.X.(*ast.Ident); ok && s.imports[id.Name] == sdkModule+"/api" && sel.Sel.Name == "HandleIntegration" {
|
|
208
|
+
mark(call.Args[1])
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
199
212
|
if !ok || len(call.Args) != 3 {
|
|
200
213
|
return true
|
|
201
214
|
}
|
|
@@ -205,7 +218,7 @@ func boundaryHandlers(sources []source) map[ast.Node]bool {
|
|
|
205
218
|
}
|
|
206
219
|
id, ok := sel.X.(*ast.Ident)
|
|
207
220
|
if ok && (s.imports[id.Name] == sdkModule+"/api" && sel.Sel.Name == "HandleHTTP" ||
|
|
208
|
-
s.imports[id.Name] == sdkModule+"/ipc" && sel.Sel.Name == "ServeUnix") {
|
|
221
|
+
s.imports[id.Name] == sdkModule+"/ipc" && (sel.Sel.Name == "ServeUnix" || sel.Sel.Name == "StartUnix")) {
|
|
209
222
|
mark(call.Args[2])
|
|
210
223
|
}
|
|
211
224
|
return true
|
|
@@ -300,6 +313,127 @@ func reusesBoundary(expr ast.Node, owner source, sources []source, safe map[ast.
|
|
|
300
313
|
return found
|
|
301
314
|
}
|
|
302
315
|
|
|
316
|
+
// Resolve declared receiver types without executing the App or requiring a
|
|
317
|
+
// successful type check (the audit also runs while projects are being edited).
|
|
318
|
+
func receiverType(expr ast.Node, src source, sources []source, seen map[ast.Node]bool) string {
|
|
319
|
+
if expr == nil || seen[expr] {
|
|
320
|
+
return ""
|
|
321
|
+
}
|
|
322
|
+
seen[expr] = true
|
|
323
|
+
follow := func(n ast.Node) string { return receiverType(n, src, sources, seen) }
|
|
324
|
+
find := func(pkg, name string) (ast.Node, source) {
|
|
325
|
+
for _, s := range sources {
|
|
326
|
+
if s.pkg != pkg {
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
for _, decl := range s.file.Decls {
|
|
330
|
+
if fn, ok := decl.(*ast.FuncDecl); ok && fn.Recv == nil && fn.Name.Name == name {
|
|
331
|
+
return fn, s
|
|
332
|
+
}
|
|
333
|
+
if gen, ok := decl.(*ast.GenDecl); ok {
|
|
334
|
+
for _, spec := range gen.Specs {
|
|
335
|
+
if typ, ok := spec.(*ast.TypeSpec); ok && typ.Name.Name == name {
|
|
336
|
+
return typ, s
|
|
337
|
+
}
|
|
338
|
+
if value, ok := spec.(*ast.ValueSpec); ok {
|
|
339
|
+
for _, id := range value.Names {
|
|
340
|
+
if id.Name == name {
|
|
341
|
+
return id, s
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return nil, src
|
|
350
|
+
}
|
|
351
|
+
switch v := expr.(type) {
|
|
352
|
+
case *ast.ParenExpr:
|
|
353
|
+
return follow(v.X)
|
|
354
|
+
case *ast.StarExpr:
|
|
355
|
+
return follow(v.X)
|
|
356
|
+
case *ast.UnaryExpr:
|
|
357
|
+
return follow(v.X)
|
|
358
|
+
case *ast.CompositeLit:
|
|
359
|
+
return follow(v.Type)
|
|
360
|
+
case *ast.SelectorExpr:
|
|
361
|
+
if id, ok := v.X.(*ast.Ident); ok && src.imports[id.Name] != "" {
|
|
362
|
+
return src.imports[id.Name] + "." + v.Sel.Name
|
|
363
|
+
}
|
|
364
|
+
typ := follow(v.X)
|
|
365
|
+
if dot := strings.LastIndex(typ, "."); dot > 0 {
|
|
366
|
+
decl, owner := find(typ[:dot], typ[dot+1:])
|
|
367
|
+
if d, ok := decl.(*ast.TypeSpec); ok {
|
|
368
|
+
if st, ok := d.Type.(*ast.StructType); ok {
|
|
369
|
+
for _, field := range st.Fields.List {
|
|
370
|
+
for _, name := range field.Names {
|
|
371
|
+
if name.Name == v.Sel.Name {
|
|
372
|
+
return receiverType(field.Type, owner, sources, seen)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
case *ast.Ident:
|
|
380
|
+
if v.Obj != nil {
|
|
381
|
+
switch d := v.Obj.Decl.(type) {
|
|
382
|
+
case *ast.Field:
|
|
383
|
+
return follow(d.Type)
|
|
384
|
+
case *ast.ValueSpec:
|
|
385
|
+
if d.Type != nil {
|
|
386
|
+
return follow(d.Type)
|
|
387
|
+
}
|
|
388
|
+
for i, name := range d.Names {
|
|
389
|
+
if name.Name == v.Name && i < len(d.Values) {
|
|
390
|
+
return follow(d.Values[i])
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
case *ast.AssignStmt:
|
|
394
|
+
for i, lhs := range d.Lhs {
|
|
395
|
+
if id, ok := lhs.(*ast.Ident); ok && id.Name == v.Name && i < len(d.Rhs) {
|
|
396
|
+
return follow(d.Rhs[i])
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
case *ast.TypeSpec:
|
|
400
|
+
if _, ok := d.Type.(*ast.StructType); ok {
|
|
401
|
+
return src.pkg + "." + d.Name.Name
|
|
402
|
+
}
|
|
403
|
+
return follow(d.Type)
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
decl, owner := find(src.pkg, v.Name)
|
|
407
|
+
return receiverType(decl, owner, sources, seen)
|
|
408
|
+
}
|
|
409
|
+
case *ast.TypeSpec:
|
|
410
|
+
if _, ok := v.Type.(*ast.StructType); ok {
|
|
411
|
+
return src.pkg + "." + v.Name.Name
|
|
412
|
+
}
|
|
413
|
+
return follow(v.Type)
|
|
414
|
+
case *ast.FuncDecl:
|
|
415
|
+
if v.Type.Results != nil && len(v.Type.Results.List) > 0 {
|
|
416
|
+
return follow(v.Type.Results.List[0].Type)
|
|
417
|
+
}
|
|
418
|
+
case *ast.CallExpr:
|
|
419
|
+
if id, ok := v.Fun.(*ast.Ident); ok && id.Name == "new" && len(v.Args) == 1 {
|
|
420
|
+
return follow(v.Args[0])
|
|
421
|
+
}
|
|
422
|
+
pkg, name := src.pkg, ""
|
|
423
|
+
switch fun := v.Fun.(type) {
|
|
424
|
+
case *ast.Ident:
|
|
425
|
+
name = fun.Name
|
|
426
|
+
case *ast.SelectorExpr:
|
|
427
|
+
if id, ok := fun.X.(*ast.Ident); ok {
|
|
428
|
+
pkg, name = src.imports[id.Name], fun.Sel.Name
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
decl, owner := find(pkg, name)
|
|
432
|
+
return receiverType(decl, owner, sources, seen)
|
|
433
|
+
}
|
|
434
|
+
return ""
|
|
435
|
+
}
|
|
436
|
+
|
|
303
437
|
func main() {
|
|
304
438
|
var in input
|
|
305
439
|
if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil {
|
|
@@ -390,7 +524,7 @@ func main() {
|
|
|
390
524
|
}
|
|
391
525
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
392
526
|
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")
|
|
527
|
+
problem(fn.Pos(), "unregistered ServeHTTP; use api.HandleHTTP, api.HandleIntegration or ipc.ServeUnix")
|
|
394
528
|
}
|
|
395
529
|
selector, ok := n.(*ast.SelectorExpr)
|
|
396
530
|
if !ok {
|
|
@@ -399,26 +533,37 @@ func main() {
|
|
|
399
533
|
if selector.Sel.Name == "ServeHTTP" && !safe[n] {
|
|
400
534
|
problem(selector.Pos(), "unregistered ServeHTTP dispatch")
|
|
401
535
|
}
|
|
536
|
+
receiver := receiverType(selector.X, src, sources, map[ast.Node]bool{})
|
|
537
|
+
if receiver == "net.ListenConfig" && (selector.Sel.Name == "Listen" || selector.Sel.Name == "ListenPacket") ||
|
|
538
|
+
receiver == "net/http.Server" && (selector.Sel.Name == "Serve" || selector.Sel.Name == "ServeTLS" ||
|
|
539
|
+
selector.Sel.Name == "ListenAndServe" || selector.Sel.Name == "ListenAndServeTLS") {
|
|
540
|
+
problem(selector.Pos(), "custom listener outside SDK boundaries: "+selector.Sel.Name)
|
|
541
|
+
}
|
|
402
542
|
pkg, ok := selector.X.(*ast.Ident)
|
|
403
543
|
if !ok {
|
|
404
544
|
return true
|
|
405
545
|
}
|
|
406
546
|
module := imports[pkg.Name]
|
|
407
547
|
name := selector.Sel.Name
|
|
408
|
-
if module == "net" && (
|
|
548
|
+
if module == "net" && (name == "Listen" || name == "ListenTCP" || name == "ListenUnix" ||
|
|
549
|
+
name == "ListenPacket" || name == "ListenUDP" || name == "ListenIP" || name == "ListenUnixgram" ||
|
|
550
|
+
name == "FileListener" || name == "FilePacketConn") ||
|
|
409
551
|
module == "crypto/tls" && name == "Listen" ||
|
|
410
552
|
(module == "syscall" || module == "golang.org/x/sys/unix") && (name == "Bind" || name == "Listen" || name == "Socket") {
|
|
411
553
|
problem(selector.Pos(), "custom listener outside SDK boundaries: "+name)
|
|
412
554
|
}
|
|
413
|
-
if module == sdkModule+"/ipc" &&
|
|
414
|
-
|
|
555
|
+
if module == sdkModule+"/ipc" && !direct[selector] {
|
|
556
|
+
switch name {
|
|
557
|
+
case "ServeUnix", "StartUnix", "ServePeerUnix", "DialUnix", "DialPeerUnix", "ReserveLoopback", "ProbeLoopback":
|
|
558
|
+
problem(selector.Pos(), "IPC constructor aliases cannot be audited")
|
|
559
|
+
}
|
|
415
560
|
}
|
|
416
561
|
if module == "github.com/synopackageland/syno-pkg-land-sdk/idgateway" && (name == "NewFront" || name == "Build" || name == "Run") {
|
|
417
562
|
problem(selector.Pos(), "gateway listeners must use identity_gateway in the manifest; upstream_auth_paths never exempts SDK RPC or custom HTTP handlers")
|
|
418
563
|
}
|
|
419
564
|
if module == "github.com/synopackageland/syno-pkg-land-sdk/api" {
|
|
420
565
|
switch name {
|
|
421
|
-
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP":
|
|
566
|
+
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP", "HandleIntegration":
|
|
422
567
|
if !direct[selector] {
|
|
423
568
|
problem(selector.Pos(), "route constructor aliases cannot be audited; register methods directly")
|
|
424
569
|
}
|
|
@@ -426,7 +571,7 @@ func main() {
|
|
|
426
571
|
}
|
|
427
572
|
if module == "net/http" {
|
|
428
573
|
switch name {
|
|
429
|
-
case "Handle", "HandleFunc", "
|
|
574
|
+
case "Handle", "HandleFunc", "Serve", "ServeTLS", "ListenAndServe", "ListenAndServeTLS":
|
|
430
575
|
problem(selector.Pos(), "HTTP handler outside the SDK registry: "+name)
|
|
431
576
|
case "HandlerFunc", "NewServeMux", "ServeMux", "FileServer", "FileServerFS", "StripPrefix", "TimeoutHandler", "NotFoundHandler", "RedirectHandler":
|
|
432
577
|
if !safe[n] {
|
|
@@ -453,6 +598,26 @@ func main() {
|
|
|
453
598
|
if !ok {
|
|
454
599
|
return true
|
|
455
600
|
}
|
|
601
|
+
// An in-process administrator call names one fixed route that
|
|
602
|
+
// already requires DSM administrators; the SDK verifies the account.
|
|
603
|
+
if selector.Sel.Name == "InvokeAsAdministrator" {
|
|
604
|
+
method := ""
|
|
605
|
+
if len(call.Args) == 4 {
|
|
606
|
+
switch arg := call.Args[2].(type) {
|
|
607
|
+
case *ast.BasicLit:
|
|
608
|
+
if arg.Kind == token.STRING {
|
|
609
|
+
method, _ = strconv.Unquote(arg.Value)
|
|
610
|
+
}
|
|
611
|
+
case *ast.Ident:
|
|
612
|
+
method = constants[arg.Name]
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if method == "" {
|
|
616
|
+
problem(call.Pos(), "dynamic InvokeAsAdministrator method cannot be audited; name a literal method")
|
|
617
|
+
} else if in.Permissions[method] != "administrators" {
|
|
618
|
+
problem(call.Pos(), "InvokeAsAdministrator requires a method declared administrators in permissions.json: "+method)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
456
621
|
pkg, ok := selector.X.(*ast.Ident)
|
|
457
622
|
if !ok {
|
|
458
623
|
return true
|
|
@@ -468,7 +633,7 @@ func main() {
|
|
|
468
633
|
return true
|
|
469
634
|
}
|
|
470
635
|
switch selector.Sel.Name {
|
|
471
|
-
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP":
|
|
636
|
+
case "Handle", "HandleRequest", "HandleResponse", "HandleStream", "HandleRequestBatch", "HandleHTTP", "HandleIntegration":
|
|
472
637
|
default:
|
|
473
638
|
return true
|
|
474
639
|
}
|
|
@@ -492,6 +657,14 @@ func main() {
|
|
|
492
657
|
} else if _, declared := in.Permissions[method]; !declared && !sdk[method] {
|
|
493
658
|
problem(call.Pos(), "registered method missing from permissions.json: "+method)
|
|
494
659
|
}
|
|
660
|
+
// Only HandleIntegration skips the DSM session; the permission
|
|
661
|
+
// value and the constructor must agree in both directions.
|
|
662
|
+
if (selector.Sel.Name == "HandleIntegration") != (in.Permissions[method] == "integration") {
|
|
663
|
+
problem(call.Pos(), "the integration permission requires api.HandleIntegration, and api.HandleIntegration requires the integration permission")
|
|
664
|
+
}
|
|
665
|
+
if selector.Sel.Name == "HandleIntegration" && method != "" {
|
|
666
|
+
out.HTTPRoutes = append(out.HTTPRoutes, httpRoute{"?method=" + method, method})
|
|
667
|
+
}
|
|
495
668
|
if selector.Sel.Name == "HandleHTTP" {
|
|
496
669
|
prefix := ""
|
|
497
670
|
if len(call.Args) > 0 {
|
|
@@ -16,5 +16,7 @@ export interface InstallWizardFiles {
|
|
|
16
16
|
* items, and only the dynamic one adds the reuse notice when data exists.
|
|
17
17
|
*/
|
|
18
18
|
export declare function renderInstallWizardFiles(packageName: string, render: (t: Translate) => InstallWizardStep[]): InstallWizardFiles;
|
|
19
|
+
/** Lists the App's unauthenticated service callbacks for the administrator. */
|
|
20
|
+
export declare function integrationWizardSteps(t: Translate, methods?: readonly string[]): InstallWizardStep[];
|
|
19
21
|
/** The native-go install step: no parameters, only the app-space consent. */
|
|
20
22
|
export declare function renderNativeInstallWizard(t: Translate): InstallWizardStep[];
|
|
@@ -70,6 +70,10 @@ export function renderInstallWizardFiles(packageName, render) {
|
|
|
70
70
|
}
|
|
71
71
|
return { static: staticFiles, dynamic };
|
|
72
72
|
}
|
|
73
|
+
/** Lists the App's unauthenticated service callbacks for the administrator. */
|
|
74
|
+
export function integrationWizardSteps(t, methods = []) {
|
|
75
|
+
return methods.length ? [{ step_title: t("integration.step"), items: [{ desc: t("integration.notice", { methods: methods.join(", ") }) }] }] : [];
|
|
76
|
+
}
|
|
73
77
|
/** The native-go install step: no parameters, only the app-space consent. */
|
|
74
78
|
export function renderNativeInstallWizard(t) {
|
|
75
79
|
return [{ step_title: t("install.step"), items: [{ desc: t("consent.appOwnSpace") }] }];
|
|
@@ -230,6 +230,28 @@ export declare const WIZARD_MESSAGES: {
|
|
|
230
230
|
es: string;
|
|
231
231
|
it: string;
|
|
232
232
|
};
|
|
233
|
+
"integration.step": {
|
|
234
|
+
en: string;
|
|
235
|
+
"zh-TW": string;
|
|
236
|
+
"zh-CN": string;
|
|
237
|
+
ja: string;
|
|
238
|
+
ko: string;
|
|
239
|
+
de: string;
|
|
240
|
+
fr: string;
|
|
241
|
+
es: string;
|
|
242
|
+
it: string;
|
|
243
|
+
};
|
|
244
|
+
"integration.notice": {
|
|
245
|
+
en: string;
|
|
246
|
+
"zh-TW": string;
|
|
247
|
+
"zh-CN": string;
|
|
248
|
+
ja: string;
|
|
249
|
+
ko: string;
|
|
250
|
+
de: string;
|
|
251
|
+
fr: string;
|
|
252
|
+
es: string;
|
|
253
|
+
it: string;
|
|
254
|
+
};
|
|
233
255
|
"install.reuseData": {
|
|
234
256
|
en: string;
|
|
235
257
|
"zh-TW": string;
|
|
@@ -230,6 +230,28 @@ export const WIZARD_MESSAGES = {
|
|
|
230
230
|
es: "Configuración de instalación",
|
|
231
231
|
it: "Impostazioni di installazione",
|
|
232
232
|
},
|
|
233
|
+
"integration.step": {
|
|
234
|
+
en: "Service callbacks",
|
|
235
|
+
"zh-TW": "服務回呼入口",
|
|
236
|
+
"zh-CN": "服务回调入口",
|
|
237
|
+
ja: "サービスコールバック",
|
|
238
|
+
ko: "서비스 콜백",
|
|
239
|
+
de: "Dienst-Callbacks",
|
|
240
|
+
fr: "Rappels de services",
|
|
241
|
+
es: "Devoluciones de llamada de servicios",
|
|
242
|
+
it: "Callback dei servizi",
|
|
243
|
+
},
|
|
244
|
+
"integration.notice": {
|
|
245
|
+
en: "These app entry points accept requests from other services without a DSM sign-in: {methods}. The app verifies each request with its own token, signature or key. It acts only for DSM accounts that are enabled and allowed by Application Privileges, and never with administrator rights.",
|
|
246
|
+
"zh-TW": "這些 App 入口接受其他服務在未登入 DSM 的情況下送來的請求:{methods}。App 會用自己的 token、簽章或金鑰驗證每個請求,只會代表已啟用且 Application Privileges 允許的 DSM 帳號執行,且一律不具管理員權限。",
|
|
247
|
+
"zh-CN": "这些应用入口接受其他服务在未登录 DSM 的情况下发送的请求:{methods}。应用会用自己的 token、签名或密钥验证每个请求,只会代表已启用且 Application Privileges 允许的 DSM 账号执行,且始终不具备管理员权限。",
|
|
248
|
+
ja: "次のアプリの入口は、DSM にサインインしていない他のサービスからの要求を受け付けます:{methods}。アプリは各要求を独自のトークン、署名、またはキーで検証します。有効で、アプリケーション権限で許可された DSM アカウントとしてのみ動作し、管理者権限は使いません。",
|
|
249
|
+
ko: "다음 앱 진입점은 DSM에 로그인하지 않은 다른 서비스의 요청을 받습니다: {methods}. 앱은 자체 토큰, 서명 또는 키로 각 요청을 확인합니다. 활성화되어 있고 애플리케이션 권한이 허용하는 DSM 계정으로만 동작하며, 관리자 권한은 사용하지 않습니다.",
|
|
250
|
+
de: "Diese App-Einstiegspunkte nehmen Anfragen anderer Dienste ohne DSM-Anmeldung an: {methods}. Die App prüft jede Anfrage mit eigenem Token, eigener Signatur oder eigenem Schlüssel. Sie handelt nur für aktivierte DSM-Konten, die die Anwendungsberechtigungen zulassen, und nie mit Administratorrechten.",
|
|
251
|
+
fr: "Ces points d'entrée de l'application acceptent des requêtes d'autres services sans connexion DSM : {methods}. L'application vérifie chaque requête avec son propre jeton, sa signature ou sa clé. Elle n'agit que pour des comptes DSM activés et autorisés par les privilèges d'application, et jamais avec des droits d'administrateur.",
|
|
252
|
+
es: "Estos puntos de entrada de la aplicación aceptan solicitudes de otros servicios sin iniciar sesión en DSM: {methods}. La aplicación verifica cada solicitud con su propio token, firma o clave. Solo actúa en nombre de cuentas de DSM habilitadas y permitidas por los privilegios de aplicación, y nunca con derechos de administrador.",
|
|
253
|
+
it: "Questi punti di ingresso dell'app accettano richieste da altri servizi senza accesso a DSM: {methods}. L'app verifica ogni richiesta con il proprio token, firma o chiave. Agisce solo per account DSM abilitati e consentiti dai privilegi delle applicazioni, mai con diritti di amministratore.",
|
|
254
|
+
},
|
|
233
255
|
"install.reuseData": {
|
|
234
256
|
en: "Data from an earlier installation is still on this NAS (@appdata/{package}), so this install reuses it: saved settings and generated secrets stay. To start empty, cancel and clear that data first — uninstall the app with “Also delete the app data listed above” ticked — then install again.",
|
|
235
257
|
"zh-TW": "這台 NAS 還留有先前安裝的資料(@appdata/{package}),本次安裝會沿用:已存的設定與產生的 secrets 都會保留。若要從空白開始,請先取消並清除該資料——解除安裝時勾選「一併刪除上列 App 資料」——再重新安裝。",
|
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,10 @@ import { checkJobs, commitOutputs, forEachArch, } from "./standalone/arch-jobs.j
|
|
|
38
38
|
import { checkReproducible } from "./reproducible.js";
|
|
39
39
|
import { spkApprovalDiff, formatApprovalDiff } from "./runtime/approval-diff.js";
|
|
40
40
|
import { inspectAnySpk } from "./spk/inspect.js";
|
|
41
|
+
import { backupRecord } from "./standalone/hyper-backup.js";
|
|
41
42
|
import { terminalSafe } from "./terminal-text.js";
|
|
43
|
+
import { auditGoRoutes } from "./standalone/route-audit.js";
|
|
44
|
+
import { parseApiPermissions } from "./standalone/api-permissions.js";
|
|
42
45
|
function failure(code, message, hint = "Fix the configuration and retry; use --help to see command usage.") {
|
|
43
46
|
return Object.assign(new Error(codedMessage(code, message)), { code, hint });
|
|
44
47
|
}
|
|
@@ -123,10 +126,14 @@ function formatRootSteps(report) {
|
|
|
123
126
|
function formatInstallSettings(settings) {
|
|
124
127
|
return settings ? Object.entries(settings).map(([key, s]) => `Installation setting ${key}: ${s.type}, ${s.required ? 'required' : 'optional'} (${s.label}); values are private`) : [];
|
|
125
128
|
}
|
|
129
|
+
function formatHyperBackup(b) {
|
|
130
|
+
return [b ? `Hyper Backup: application backup on, ${b.online ? 'online (App keeps running; SQLite snapshots)' : 'offline (App stopped during export)'}, data version ${b.dataVersion}${b.hook ? `, hook ${b.hook}` : ''}. Hyper Backup runs the fixed backup scripts as root; they only switch to the package account.` : 'Hyper Backup: application backup off'];
|
|
131
|
+
}
|
|
126
132
|
function formatSpkReport(spk, report) {
|
|
127
133
|
return [
|
|
128
134
|
`SPK: ${spk}`,
|
|
129
135
|
...formatInstallSettings(report.installSettings),
|
|
136
|
+
...(report.synopkgland ? formatHyperBackup(report.hyperBackup) : []),
|
|
130
137
|
...(report.identityGateway ? [report.identityGateway.notice, `OIDC issuer access: ${report.identityGateway.issuerAccess}`] : []),
|
|
131
138
|
`package: ${report.package ?? "?"} version: ${report.version ?? "?"}`,
|
|
132
139
|
report.synopkgland
|
|
@@ -335,6 +342,30 @@ async function runCliWithin(argv) {
|
|
|
335
342
|
.option("--json", "Print a single JSON object")
|
|
336
343
|
.action(async (id, options) => { const { uninstallPackage } = await import('./host/package-commands.js'); const result = await uninstallPackage(id, options); emit('uninstall', result, JSON.stringify(result, null, 2)); });
|
|
337
344
|
program.command("ctl").description("Control registered NAS Apps using described AI commands (ctl --help)");
|
|
345
|
+
program.command("audit-entry <main>")
|
|
346
|
+
.description("Audit one custom Go payload's target import graph without executing it")
|
|
347
|
+
.option("--dir <dir>", "Go module directory", ".")
|
|
348
|
+
.option("--arch <arches>", "Comma-separated Go target architectures", "amd64,arm64,arm")
|
|
349
|
+
.option("--permissions <file>", "App permissions JSON, when the payload registers App methods")
|
|
350
|
+
.option("--json", "Print a single JSON object")
|
|
351
|
+
.action((main, options) => {
|
|
352
|
+
const arches = options.arch.split(",");
|
|
353
|
+
if (!arches.length || arches.some(arch => !["amd64", "arm64", "arm"].includes(arch))) {
|
|
354
|
+
throw new Error("audit-entry: --arch must contain amd64, arm64 or arm");
|
|
355
|
+
}
|
|
356
|
+
const dir = resolve(options.dir);
|
|
357
|
+
const policy = options.permissions
|
|
358
|
+
? parseApiPermissions(readFileSync(resolve(dir, options.permissions)))
|
|
359
|
+
: { schema: 1, default: "administrators", methods: {} };
|
|
360
|
+
const audit = auditGoRoutes(dir, policy, {}, [main], arches);
|
|
361
|
+
const ok = audit.problems.length === 0;
|
|
362
|
+
if (json)
|
|
363
|
+
stdout(JSON.stringify({ ok, ...audit }));
|
|
364
|
+
else
|
|
365
|
+
stdout(terminalSafe(ok ? "Entry audit passed" : audit.problems.join("\n")));
|
|
366
|
+
if (!ok)
|
|
367
|
+
process.exitCode = 1;
|
|
368
|
+
});
|
|
338
369
|
program
|
|
339
370
|
.command("check")
|
|
340
371
|
.description("Validate the manifest, capability combination and Compose")
|
|
@@ -375,6 +406,7 @@ async function runCliWithin(argv) {
|
|
|
375
406
|
package: loaded.manifest.package,
|
|
376
407
|
warnings: validation.warnings ?? [],
|
|
377
408
|
...("installSettings" in loaded.manifest && loaded.manifest.installSettings ? { installSettings: loaded.manifest.installSettings } : {}),
|
|
409
|
+
...("hyperBackup" in loaded.manifest && loaded.manifest.hyperBackup ? { hyperBackup: backupRecord(loaded.manifest.hyperBackup) } : {}),
|
|
378
410
|
...("identityGateway" in loaded.manifest && gatewayAdmissionReport(loaded.manifest.identityGateway, loaded.manifest.entry) ? { identityGateway: gatewayAdmissionReport(loaded.manifest.identityGateway, loaded.manifest.entry) } : {}),
|
|
379
411
|
capabilities: "capabilities" in loaded.manifest
|
|
380
412
|
? loaded.manifest.capabilities
|
|
@@ -397,6 +429,7 @@ async function runCliWithin(argv) {
|
|
|
397
429
|
}, [
|
|
398
430
|
"Validation passed.",
|
|
399
431
|
...formatInstallSettings("installSettings" in loaded.manifest ? loaded.manifest.installSettings : undefined),
|
|
432
|
+
...("capabilities" in loaded.manifest ? formatHyperBackup(loaded.manifest.hyperBackup) : []),
|
|
400
433
|
...("identityGateway" in loaded.manifest && gatewayAdmissionReport(loaded.manifest.identityGateway) ? [gatewayAdmissionReport(loaded.manifest.identityGateway).notice] : []),
|
|
401
434
|
...("compose" in loaded.manifest && loaded.manifest.compose
|
|
402
435
|
? [
|
package/dist/error-registry.js
CHANGED
|
@@ -15,6 +15,11 @@ export const ERROR_REGISTRY = {
|
|
|
15
15
|
INSTALL_SETTINGS_INVALID: { "code": "INSTALL_SETTINGS_INVALID", "description": "The installation setting declaration is invalid.", "suggestion": "Declare 1–32 lowercase identifier keys in install.settings, each with type https-origin, a plain label and an explicit boolean required; omit defaults and values." },
|
|
16
16
|
SDK_INSTALL_SETTINGS_UNSUPPORTED: { "code": "SDK_INSTALL_SETTINGS_UNSUPPORTED", "description": "The selected SDK does not provide native installation settings.", "suggestion": "Pin SDK 0.24.0 or a matching supported release with installsettings/cmd/control, then rebuild with the matching CLI." },
|
|
17
17
|
SDK_INSTALL_SETTINGS_BUILD_FAILED: { "code": "SDK_INSTALL_SETTINGS_BUILD_FAILED", "description": "The SDK installation settings controller could not be built.", "suggestion": "Install the pinned Go toolchain, inspect the compiler diagnostics and rebuild with a complete matching SDK release." },
|
|
18
|
+
BACKUP_INVALID: { code: "BACKUP_INVALID", description: "The Hyper Backup application backup declaration or its packaged scripts are invalid.", suggestion: "Use backup.hyper_backup: false to opt out, or only online, data_version (\"major.minor\") and hook; rebuild so scripts/backup holds exactly the fixed export, import, info and version files." },
|
|
19
|
+
BACKUP_COMPOSE_UNSUPPORTED: { code: "BACKUP_COMPOSE_UNSUPPORTED", description: "Compose Apps keep their data in container volumes, which application backup does not cover.", suggestion: "Remove backup from a Compose App or set backup.hyper_backup: false." },
|
|
20
|
+
BACKUP_HOOK_MISSING: { code: "BACKUP_HOOK_MISSING", description: "backup.hook does not name an executable file in the package.", suggestion: "Package the hook (for example in the daemon payload or the App folder) and give its path relative to the package target." },
|
|
21
|
+
SDK_APP_BACKUP_UNSUPPORTED: { code: "SDK_APP_BACKUP_UNSUPPORTED", description: "The selected SDK does not provide Hyper Backup application backup.", suggestion: "Pin an SDK release with appbackup/cmd/control, or remove the backup declaration; without one the build skips application backup." },
|
|
22
|
+
SDK_APP_BACKUP_BUILD_FAILED: { code: "SDK_APP_BACKUP_BUILD_FAILED", description: "The SDK backup control could not be built.", suggestion: "Install the pinned Go toolchain, inspect the compiler diagnostics and rebuild with a complete matching SDK release." },
|
|
18
23
|
INIT_HEALTH_INVALID: { code: "INIT_HEALTH_INVALID", description: "The Compose readiness selection is invalid.", suggestion: "Supply both --health-path and --health-status with the actual endpoint and expected 2xx response; redirects are not followed." },
|
|
19
24
|
INIT_UPSTREAM_IMAGE_REQUIRED: { code: "INIT_UPSTREAM_IMAGE_REQUIRED", description: "The official upstream image repository was not selected.", suggestion: "Use init --upstream-fork --upstream-image <official-repository> for Compose." },
|
|
20
25
|
INIT_UPSTREAM_IMAGE_INVALID: { code: "INIT_UPSTREAM_IMAGE_INVALID", description: "The upstream image repository is invalid.", suggestion: "Supply the official repository without a tag or digest; upgrade.sh selects the matching release and resolves its digest." },
|
|
@@ -843,6 +848,16 @@ export const ERROR_REGISTRY = {
|
|
|
843
848
|
description: "The API policy is invalid.",
|
|
844
849
|
suggestion: "Fix backend/internal/api/permissions.json.",
|
|
845
850
|
},
|
|
851
|
+
STANDALONE_INTEGRATIONS_INVALID: {
|
|
852
|
+
code: "STANDALONE_INTEGRATIONS_INVALID",
|
|
853
|
+
description: "The integrations list in dsm-package.yaml is invalid or used where an integration callback cannot run.",
|
|
854
|
+
suggestion: "List each integration method once, sorted, as a string; integrations need a Go backend without compose.",
|
|
855
|
+
},
|
|
856
|
+
STANDALONE_INTEGRATIONS_MISMATCH: {
|
|
857
|
+
code: "STANDALONE_INTEGRATIONS_MISMATCH",
|
|
858
|
+
description: "The integrations list does not match the methods permissions.json marks integration.",
|
|
859
|
+
suggestion: "List exactly the integration methods from permissions.json in dsm-package.yaml integrations.",
|
|
860
|
+
},
|
|
846
861
|
CLI_OPTION_UNKNOWN: {
|
|
847
862
|
code: "CLI_OPTION_UNKNOWN",
|
|
848
863
|
description: "Unknown CLI option.",
|
package/dist/skill.js
CHANGED
|
@@ -52,6 +52,11 @@ const SKILL_CATALOG = [
|
|
|
52
52
|
file: "reference/lifecycle.md",
|
|
53
53
|
when: "Add work to install, upgrade, uninstall, start or stop (dsm-scripts/)",
|
|
54
54
|
},
|
|
55
|
+
{
|
|
56
|
+
id: "backup",
|
|
57
|
+
file: "reference/backup.md",
|
|
58
|
+
when: "Change or opt out of Hyper Backup application backup",
|
|
59
|
+
},
|
|
55
60
|
{ id: "deploy", file: "reference/deploy.md", when: "Deploy to a NAS" },
|
|
56
61
|
{
|
|
57
62
|
id: "diagnose",
|
|
@@ -5,7 +5,8 @@ export declare const API_PERMISSIONS_FILE: Record<SdkBackend["kind"], string>;
|
|
|
5
5
|
type SdkPermissions = Record<string, Record<string, ApiPermission>>;
|
|
6
6
|
export declare const MAX_PERMISSIONS_BYTES = 65536;
|
|
7
7
|
export declare const MAX_PERMISSION_METHODS = 1024;
|
|
8
|
-
|
|
8
|
+
/** `integration` marks a Go `api.HandleIntegration` callback that arrives without a DSM session. */
|
|
9
|
+
export type ApiPermission = "administrators" | "authenticated" | "public" | "integration";
|
|
9
10
|
export interface ApiPermissions {
|
|
10
11
|
schema: 1;
|
|
11
12
|
default: "administrators";
|
|
@@ -30,4 +31,6 @@ export declare function floorAtAdministrators(policy: ApiPermissions, sdkPermiss
|
|
|
30
31
|
};
|
|
31
32
|
/** Launcher access is derived from the same API policy; no second setting. */
|
|
32
33
|
export declare function permitsOrdinaryUsers(policy?: ApiPermissions): boolean;
|
|
34
|
+
/** Methods declared with the `integration` permission, sorted. */
|
|
35
|
+
export declare function integrationMethods(policy?: ApiPermissions): string[];
|
|
33
36
|
export {};
|
|
@@ -27,7 +27,7 @@ export function parseApiPermissions(raw) {
|
|
|
27
27
|
throw new Error("too many permission methods");
|
|
28
28
|
for (const [method, permission] of entries) {
|
|
29
29
|
const matched = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.exec(method);
|
|
30
|
-
if (method.length > 128 || matched?.[0] !== method || !["administrators", "authenticated", "public"].includes(permission)) {
|
|
30
|
+
if (method.length > 128 || matched?.[0] !== method || !["administrators", "authenticated", "public", "integration"].includes(permission)) {
|
|
31
31
|
throw new Error(`invalid permission for method ${JSON.stringify(method)}`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -63,11 +63,17 @@ export function readApiPermissions(backend, capabilities = {}) {
|
|
|
63
63
|
export function floorAtAdministrators(policy, sdkPermissions) {
|
|
64
64
|
const sdk = new Set(Object.values(sdkPermissions).flatMap(methods => Object.keys(methods)));
|
|
65
65
|
const methods = Object.fromEntries(Object.entries(policy.methods)
|
|
66
|
-
.map(([method, permission]) => [method, sdk.has(method) && permission === "public" ? permission : "administrators"]));
|
|
67
|
-
|
|
66
|
+
.map(([method, permission]) => [method, sdk.has(method) && permission === "public" || permission === "integration" ? permission : "administrators"]));
|
|
67
|
+
// An integration callback admits no DSM account by itself: api.ActAs refuses
|
|
68
|
+
// every account in an administrator-only App.
|
|
69
|
+
const widened = Object.entries(policy.methods).filter(([method, permission]) => !sdk.has(method) && permission !== "administrators" && permission !== "integration").map(([method]) => method);
|
|
68
70
|
return { policy: { ...policy, methods }, widened };
|
|
69
71
|
}
|
|
70
72
|
/** Launcher access is derived from the same API policy; no second setting. */
|
|
71
73
|
export function permitsOrdinaryUsers(policy) {
|
|
72
|
-
return policy !== undefined && Object.values(policy.methods).some(permission => permission !== "administrators");
|
|
74
|
+
return policy !== undefined && Object.values(policy.methods).some(permission => permission !== "administrators" && permission !== "integration");
|
|
75
|
+
}
|
|
76
|
+
/** Methods declared with the `integration` permission, sorted. */
|
|
77
|
+
export function integrationMethods(policy) {
|
|
78
|
+
return Object.entries(policy?.methods ?? {}).filter(([, permission]) => permission === "integration").map(([method]) => method).sort();
|
|
73
79
|
}
|