@synopackageland/templates 0.9.0 → 0.9.1
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/README.md +1 -1
- package/package.json +1 -1
- package/templates/_shared/AGENTS.md +7 -4
- package/templates/native-go/README.md +5 -1
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/backend/internal/app/delegate_config.go +1 -1
- package/templates/native-go/backend/internal/app/delegate_config_test.go +8 -0
- package/templates/native-go/backend/internal/app/routes_test.go +13 -3
- package/templates/native-go-root-ops/backend/internal/app/executor.go +29 -41
- package/templates/native-go-root-ops/backend/internal/app/executor_test.go +130 -11
- package/templates/native-go-vue/README.md +2 -2
- package/templates/native-go-vue/frontend/package-lock.json +4 -4
- package/templates/native-go-vue/frontend/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ An overlay may replace `internal/app/*_none.go` (the CLI deletes the matching
|
|
|
21
21
|
The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
|
|
22
22
|
`__BACKGROUND_ENABLED__` are substituted by `init`.
|
|
23
23
|
|
|
24
|
-
`backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.9.
|
|
24
|
+
`backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.9.1`; that line is
|
|
25
25
|
the single source of the SDK version and `frontend/package.json` pins
|
|
26
26
|
`@synopackageland/sdk` to the same version.
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/templates",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Project templates for synopkgland init: native-go (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {"type": "git", "url": "git+https://github.com/synopackageland/syno-pkg-land-templates.git"},
|
|
@@ -32,10 +32,13 @@ call `greeting.set`.
|
|
|
32
32
|
|
|
33
33
|
## DSM Application Portal (`portal`)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
Only an App with the root executor (`rootOps` or `delegate`) may declare a
|
|
36
|
+
portal: its uninstall then reloads nginx so the alias stops answering, which a
|
|
37
|
+
no-privilege App cannot do without root. `synopkgland add portal` (with
|
|
38
|
+
`root-ops` or `delegate` in the same call or already declared; or `portal: {}` /
|
|
39
|
+
`portal: {alias: my-app}` in dsm-package.yaml) registers
|
|
40
|
+
`https://<nas>/<alias>/`, which opens the app after DSM sign-in. Compose Apps
|
|
41
|
+
cannot declare a portal. Read `synopkgland skill show portal` before changing it.
|
|
39
42
|
The install wizard only proposes the alias; an administrator activates it from
|
|
40
43
|
the app's portal panel (`ctx.portal`, DSM's own `SYNO.Core.AppPortal` in the
|
|
41
44
|
admin's session) or Control Panel > Login Portal > Applications. Ordinary users
|
|
@@ -297,7 +297,11 @@ refusal for unknown methods.
|
|
|
297
297
|
|
|
298
298
|
## DSM Application Portal link
|
|
299
299
|
|
|
300
|
-
|
|
300
|
+
Only an App with the root executor (`rootOps` or `delegate`) can declare a
|
|
301
|
+
portal; `synopkgland add root-ops portal` declares both. Uninstall then
|
|
302
|
+
removes the App's Application Privileges rules and reloads nginx once DSM has
|
|
303
|
+
removed the alias, so it stops answering. `synopkgland add portal` adds
|
|
304
|
+
`portal: {}` to dsm-package.yaml so
|
|
301
305
|
`https://<nas>/<alias>/` opens this app right after DSM sign-in. The install
|
|
302
306
|
wizard proposes the alias (the package name in lowercase by default); an
|
|
303
307
|
administrator turns it on from the app's portal panel or Control Panel >
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.9.
|
|
2
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.9.
|
|
1
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.9.1 h1:Mni+769M/fldZTlv0IOZKeaK3xGu6hTSkTeWIALPqBg=
|
|
2
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.9.1/go.mod h1:JNoqISvR4biA9IuLi0CFe4c272tsKzCN9Nyj71nrXsQ=
|
|
3
3
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
4
4
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
@@ -32,7 +32,7 @@ func ParseDelegateConfig(cfg Config) (DelegateSettings, error) {
|
|
|
32
32
|
seen := map[string]bool{}
|
|
33
33
|
for _, op := range out.Operations {
|
|
34
34
|
switch op {
|
|
35
|
-
case "files.list", "files.info", "files.createFolder", "files.checkPermission", "files.write":
|
|
35
|
+
case "files.list", "files.info", "files.createFolder", "files.checkPermission", "files.write", "files.read", "files.rename", "files.copy", "files.move", "files.delete":
|
|
36
36
|
default:
|
|
37
37
|
return DelegateSettings{}, invalid
|
|
38
38
|
}
|
|
@@ -26,3 +26,11 @@ func TestDelegateBuildConfigStrictlySeparatesLegacyAndPermanent(t *testing.T) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
func TestDelegateBuildConfigAcceptsEveryTimeLimitedFileOperation(t *testing.T) {
|
|
31
|
+
ops := `["files.checkPermission","files.copy","files.createFolder","files.delete","files.info","files.list","files.move","files.read","files.rename","files.write"]`
|
|
32
|
+
settings, err := ParseDelegateConfig(Config{DelegateScope: "shared:media", DelegateOperationsJSON: ops, DelegateDestinationsJSON: `[]`})
|
|
33
|
+
if err != nil || len(settings.Operations) != 10 || settings.Lifetime != "" {
|
|
34
|
+
t.Fatal(settings, err)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -60,13 +60,23 @@ func call(t *testing.T, cfg Config, auth api.Authenticator, method, params strin
|
|
|
60
60
|
if len(permissionJSON) != 0 {
|
|
61
61
|
permissions = permissionJSON[0]
|
|
62
62
|
}
|
|
63
|
+
return serve(t, cfg, auth, apiRequest(method, params), permissions)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func apiRequest(method, params string) *http.Request {
|
|
67
|
+
body := `{"method":"` + method + `","params":` + params + `}`
|
|
68
|
+
req := httptest.NewRequest(http.MethodPost, "/api.cgi", strings.NewReader(body))
|
|
69
|
+
req.Header.Set("Content-Type", "application/json")
|
|
70
|
+
return req
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// serve runs req through the App's server and returns as call does.
|
|
74
|
+
func serve(t *testing.T, cfg Config, auth api.Authenticator, req *http.Request, permissions []byte) (string, map[string]any) {
|
|
75
|
+
t.Helper()
|
|
63
76
|
srv, err := api.NewServerWithPermissions(auth, permissions, Routes(cfg)...)
|
|
64
77
|
if err != nil {
|
|
65
78
|
t.Fatal(err)
|
|
66
79
|
}
|
|
67
|
-
body := `{"method":"` + method + `","params":` + params + `}`
|
|
68
|
-
req := httptest.NewRequest(http.MethodPost, "/api.cgi", strings.NewReader(body))
|
|
69
|
-
req.Header.Set("Content-Type", "application/json")
|
|
70
80
|
rec := httptest.NewRecorder()
|
|
71
81
|
srv.ServeHTTP(rec, req)
|
|
72
82
|
var out map[string]any
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
package app
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"bufio"
|
|
5
4
|
"context"
|
|
6
5
|
"encoding/json"
|
|
7
6
|
"errors"
|
|
8
|
-
"net"
|
|
9
7
|
"net/http"
|
|
10
8
|
"os"
|
|
11
9
|
"path/filepath"
|
|
12
10
|
"strings"
|
|
13
|
-
"syscall"
|
|
14
11
|
"time"
|
|
15
12
|
|
|
16
13
|
"github.com/synopackageland/syno-pkg-land-sdk/api"
|
|
14
|
+
"github.com/synopackageland/syno-pkg-land-sdk/delegate"
|
|
17
15
|
)
|
|
18
16
|
|
|
19
17
|
func (cfg Config) rootRoutes() []api.Route {
|
|
@@ -56,8 +54,10 @@ func (cfg Config) executorControl(_ context.Context, caller api.Caller, raw json
|
|
|
56
54
|
// The executor closes its socket while paused: the package is stopped, or an
|
|
57
55
|
// upgrade changed the pins and awaits administrator approval.
|
|
58
56
|
func (cfg Config) executorStatus(ctx context.Context, _ api.Caller, _ json.RawMessage) (any, error) {
|
|
59
|
-
status
|
|
60
|
-
|
|
57
|
+
var status json.RawMessage
|
|
58
|
+
err := cfg.executor().Call(ctx, "status", nil, nil, &status)
|
|
59
|
+
var refusal *api.Error
|
|
60
|
+
if errors.As(err, &refusal) && refusal.Code == delegate.CodeExecutorUnavailable {
|
|
61
61
|
next := "Start the package."
|
|
62
62
|
if cfg.startRequested() {
|
|
63
63
|
next = "The package is running: an administrator approves background access again in Administrator setup; the executor log names the cause."
|
|
@@ -95,46 +95,34 @@ func (cfg Config) executorRootRun(ctx context.Context, caller api.Caller, req *h
|
|
|
95
95
|
if err != nil {
|
|
96
96
|
return nil, err
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// executorSocketBase is a variable so tests can use a temporary directory.
|
|
102
|
-
var executorSocketBase = "/run/synopkgland"
|
|
103
|
-
|
|
104
|
-
func (cfg Config) executorRequest(ctx context.Context, op string, auth, params any) (any, error) {
|
|
105
|
-
conn, err := (&net.Dialer{Timeout: 5 * time.Second}).DialContext(ctx, "unix", filepath.Join(executorSocketBase, cfg.PackageID, "executor.sock"))
|
|
106
|
-
if err != nil {
|
|
107
|
-
return nil, err
|
|
108
|
-
}
|
|
109
|
-
defer conn.Close()
|
|
110
|
-
if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
|
98
|
+
if err := cfg.executorHoldsThisBuild(ctx); err != nil {
|
|
111
99
|
return nil, err
|
|
112
100
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
if params == nil {
|
|
117
|
-
params = map[string]any{}
|
|
118
|
-
}
|
|
119
|
-
if err := json.NewEncoder(conn).Encode(map[string]any{"v": 1, "op": op, "auth": auth, "params": params}); err != nil {
|
|
101
|
+
var out json.RawMessage
|
|
102
|
+
if err := cfg.executor().Call(ctx, "root.run", auth, map[string]string{"name": in.Name}, &out); err != nil {
|
|
120
103
|
return nil, err
|
|
121
104
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
Message string `json:"message"`
|
|
132
|
-
}
|
|
133
|
-
if err := json.Unmarshal(scanner.Bytes(), &response); err != nil {
|
|
134
|
-
return nil, err
|
|
105
|
+
return out, nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// executorHoldsThisBuild refuses, as delegate does, an executor pinned to
|
|
109
|
+
// another build's policy or executor: its approved scripts are not this build's.
|
|
110
|
+
func (cfg Config) executorHoldsThisBuild(ctx context.Context) error {
|
|
111
|
+
var pins delegate.ExecutorStatus
|
|
112
|
+
if err := cfg.executor().Call(ctx, "status", nil, nil, &pins); err != nil {
|
|
113
|
+
return err
|
|
135
114
|
}
|
|
136
|
-
if
|
|
137
|
-
return
|
|
115
|
+
if (cfg.ExecutorDigest != "" && pins.ExecutorDigest != cfg.ExecutorDigest) || (cfg.PolicyDigest != "" && pins.PolicyDigest != cfg.PolicyDigest) {
|
|
116
|
+
return &api.Error{Code: delegate.CodePolicyDenied, Message: "the executor holds another build's approved policy; an administrator approves this build in Administrator setup"}
|
|
138
117
|
}
|
|
139
|
-
return
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// executorSocketBase is a variable so tests can use a temporary directory.
|
|
122
|
+
var executorSocketBase = "/run/synopkgland"
|
|
123
|
+
|
|
124
|
+
// executor keeps the executor's protocol v1 refusal codes (POLICY_DENIED,
|
|
125
|
+
// EXECUTOR_UNAVAILABLE, ...) as the API error code.
|
|
126
|
+
func (cfg Config) executor() *delegate.Client {
|
|
127
|
+
return &delegate.Client{Socket: filepath.Join(executorSocketBase, cfg.PackageID, "executor.sock"), Timeout: 30 * time.Second}
|
|
140
128
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
package app
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
4
6
|
"net"
|
|
5
7
|
"os"
|
|
6
8
|
"path/filepath"
|
|
7
9
|
"strings"
|
|
10
|
+
"sync"
|
|
8
11
|
"testing"
|
|
12
|
+
|
|
13
|
+
appapi "__PACKAGE__/internal/api"
|
|
9
14
|
)
|
|
10
15
|
|
|
11
16
|
func init() {
|
|
@@ -59,14 +64,7 @@ func TestExecutorRootRunRequiresCurrentDSMCookie(t *testing.T) {
|
|
|
59
64
|
// awaiting administrator approval). Status then answers, not "internal".
|
|
60
65
|
func TestExecutorStatusReportsAPausedExecutor(t *testing.T) {
|
|
61
66
|
cfg := testConfig(t)
|
|
62
|
-
|
|
63
|
-
// A short base: unix socket paths are limited to about 100 bytes.
|
|
64
|
-
base, err := os.MkdirTemp("/tmp", "exec")
|
|
65
|
-
if err != nil {
|
|
66
|
-
t.Fatal(err)
|
|
67
|
-
}
|
|
68
|
-
executorSocketBase = base
|
|
69
|
-
defer func() { executorSocketBase = previous; os.RemoveAll(base) }()
|
|
67
|
+
useExecutorSocketBase(t)
|
|
70
68
|
check := func(name string) {
|
|
71
69
|
t.Helper()
|
|
72
70
|
code, envelope := call(t, cfg, alice, "executor.status", `{}`)
|
|
@@ -94,9 +92,7 @@ func TestExecutorStatusReportsAPausedExecutor(t *testing.T) {
|
|
|
94
92
|
// again, not a start.
|
|
95
93
|
func TestExecutorStatusNextFollowsTheLifecycleRequest(t *testing.T) {
|
|
96
94
|
cfg := testConfig(t)
|
|
97
|
-
|
|
98
|
-
executorSocketBase = t.TempDir()
|
|
99
|
-
defer func() { executorSocketBase = previous }()
|
|
95
|
+
useExecutorSocketBase(t)
|
|
100
96
|
next := func() string {
|
|
101
97
|
t.Helper()
|
|
102
98
|
_, envelope := call(t, cfg, alice, "executor.status", `{}`)
|
|
@@ -116,3 +112,126 @@ func TestExecutorStatusNextFollowsTheLifecycleRequest(t *testing.T) {
|
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
114
|
}
|
|
115
|
+
|
|
116
|
+
// fakeExecutor answers each v1 request with replies[op] and records the ops.
|
|
117
|
+
type fakeExecutor struct {
|
|
118
|
+
mu sync.Mutex
|
|
119
|
+
ops []string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (f *fakeExecutor) received() []string {
|
|
123
|
+
f.mu.Lock()
|
|
124
|
+
defer f.mu.Unlock()
|
|
125
|
+
return append([]string(nil), f.ops...)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func startFakeExecutor(t *testing.T, cfg Config, replies map[string]string) *fakeExecutor {
|
|
129
|
+
t.Helper()
|
|
130
|
+
useExecutorSocketBase(t)
|
|
131
|
+
dir := filepath.Join(executorSocketBase, cfg.PackageID)
|
|
132
|
+
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
133
|
+
t.Fatal(err)
|
|
134
|
+
}
|
|
135
|
+
ln, err := net.Listen("unix", filepath.Join(dir, "executor.sock"))
|
|
136
|
+
if err != nil {
|
|
137
|
+
t.Fatal(err)
|
|
138
|
+
}
|
|
139
|
+
t.Cleanup(func() { ln.Close() })
|
|
140
|
+
f := &fakeExecutor{}
|
|
141
|
+
go func() {
|
|
142
|
+
for {
|
|
143
|
+
conn, err := ln.Accept()
|
|
144
|
+
if err != nil {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
var req struct{ Op string }
|
|
148
|
+
line, _ := bufio.NewReader(conn).ReadBytes('\n')
|
|
149
|
+
json.Unmarshal(line, &req)
|
|
150
|
+
f.mu.Lock()
|
|
151
|
+
f.ops = append(f.ops, req.Op)
|
|
152
|
+
f.mu.Unlock()
|
|
153
|
+
conn.Write([]byte(replies[req.Op] + "\n"))
|
|
154
|
+
conn.Close()
|
|
155
|
+
}
|
|
156
|
+
}()
|
|
157
|
+
return f
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// useExecutorSocketBase points the CGI at a short temporary directory: unix
|
|
161
|
+
// socket paths are limited to about 100 bytes.
|
|
162
|
+
func useExecutorSocketBase(t *testing.T) {
|
|
163
|
+
t.Helper()
|
|
164
|
+
previous := executorSocketBase
|
|
165
|
+
base, err := os.MkdirTemp("/tmp", "exec")
|
|
166
|
+
if err != nil {
|
|
167
|
+
t.Fatal(err)
|
|
168
|
+
}
|
|
169
|
+
executorSocketBase = base
|
|
170
|
+
t.Cleanup(func() { executorSocketBase = previous; os.RemoveAll(base) })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// rootRun calls root.run as an administrator with a current DSM session.
|
|
174
|
+
func rootRun(t *testing.T, cfg Config, name string) (string, map[string]any) {
|
|
175
|
+
t.Helper()
|
|
176
|
+
permissions, err := appapi.Permissions()
|
|
177
|
+
if err != nil {
|
|
178
|
+
t.Fatal(err)
|
|
179
|
+
}
|
|
180
|
+
req := apiRequest("root.run", `{"name":"`+name+`"}`)
|
|
181
|
+
req.Header.Set("Cookie", "id=session")
|
|
182
|
+
req.Header.Set("X-SYNO-TOKEN", "token")
|
|
183
|
+
return serve(t, cfg, alice, req, permissions)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const readyStatus = `{"ok":true,"result":{"executor_digest":"e1","policy_digest":"p1","ready":true,"disabled":false,"version":"1"}}`
|
|
187
|
+
|
|
188
|
+
func pinnedConfig(t *testing.T) Config {
|
|
189
|
+
cfg := testConfig(t)
|
|
190
|
+
cfg.ExecutorDigest, cfg.PolicyDigest = "e1", "p1"
|
|
191
|
+
return cfg
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func TestExecutorRootRunRunsWhenTheExecutorHoldsThisBuildsPins(t *testing.T) {
|
|
195
|
+
cfg := pinnedConfig(t)
|
|
196
|
+
startFakeExecutor(t, cfg, map[string]string{"status": readyStatus, "root.run": `{"ok":true,"result":{"output":"0\n"}}`})
|
|
197
|
+
code, out := rootRun(t, cfg, "whoami")
|
|
198
|
+
if data, _ := out["data"].(map[string]any); code != "" || data["output"] != "0\n" {
|
|
199
|
+
t.Fatalf("%s %v", code, out)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Like delegate: an executor pinned to another build's policy or executor
|
|
204
|
+
// refuses the operation, it does not run it.
|
|
205
|
+
func TestExecutorRootRunRefusesAnExecutorPinnedToAnotherBuild(t *testing.T) {
|
|
206
|
+
for name, status := range map[string]string{
|
|
207
|
+
"policy": strings.Replace(readyStatus, `"p1"`, `"p2"`, 1),
|
|
208
|
+
"executor": strings.Replace(readyStatus, `"e1"`, `"e2"`, 1),
|
|
209
|
+
} {
|
|
210
|
+
cfg := pinnedConfig(t)
|
|
211
|
+
f := startFakeExecutor(t, cfg, map[string]string{"status": status, "root.run": `{"ok":true,"result":{"output":"0\n"}}`})
|
|
212
|
+
if code, out := rootRun(t, cfg, "whoami"); code != "POLICY_DENIED" {
|
|
213
|
+
t.Errorf("%s digest mismatch: %q %v", name, code, out)
|
|
214
|
+
}
|
|
215
|
+
for _, op := range f.received() {
|
|
216
|
+
if op == "root.run" {
|
|
217
|
+
t.Errorf("%s digest mismatch: root.run reached the executor", name)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Refusals keep the protocol v1 code (spec/protocol/v1/errors.json), never "internal".
|
|
224
|
+
func TestExecutorRootRunReportsTheExecutorsRefusalCode(t *testing.T) {
|
|
225
|
+
cfg := pinnedConfig(t)
|
|
226
|
+
useExecutorSocketBase(t)
|
|
227
|
+
if code, _ := rootRun(t, cfg, "whoami"); code != "EXECUTOR_UNAVAILABLE" {
|
|
228
|
+
t.Errorf("paused or awaiting approval (no socket): %q", code)
|
|
229
|
+
}
|
|
230
|
+
for _, refusal := range []string{"POLICY_DENIED", "INVALID_REQUEST", "EXECUTOR_PAUSED", "ROOT_FAILED", "ADMIN_REQUIRED"} {
|
|
231
|
+
cfg := pinnedConfig(t)
|
|
232
|
+
startFakeExecutor(t, cfg, map[string]string{"status": readyStatus, "root.run": `{"ok":false,"code":"` + refusal + `","message":"refused"}`})
|
|
233
|
+
if code, out := rootRun(t, cfg, "whoami"); code != refusal {
|
|
234
|
+
t.Errorf("%s: %q %v", refusal, code, out)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -9,7 +9,7 @@ files, portal, jobs and delegate components come from `@synopackageland/sdk/vue`
|
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
11
|
cd frontend
|
|
12
|
-
npm
|
|
12
|
+
npm ci
|
|
13
13
|
npm test
|
|
14
14
|
npm run build
|
|
15
15
|
```
|
|
@@ -22,7 +22,7 @@ subscriptions; unmount disposes subscriptions and ignores late responses.
|
|
|
22
22
|
operations. `--with jobs` adds package-account jobs and schedules; it combines with root-ops and files. `--with files` enables current-session File Station access,
|
|
23
23
|
and the background picker uses the retained session provider.
|
|
24
24
|
|
|
25
|
-
With `portal:` declared (`synopkgland add portal
|
|
25
|
+
With `portal:` declared (`synopkgland add portal`, executor Apps only), the status page shows
|
|
26
26
|
the SDK's `PortalSettings`: the DSM Application Portal link, the alias form
|
|
27
27
|
for administrators, and the Control Panel shortcut.
|
|
28
28
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "native-dsm-vue-app",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@lucide/vue": "1.48.0",
|
|
10
|
-
"@synopackageland/sdk": "0.9.
|
|
10
|
+
"@synopackageland/sdk": "0.9.1",
|
|
11
11
|
"@synopackageland/ui": "0.9.0",
|
|
12
12
|
"vue": "3.5.43"
|
|
13
13
|
},
|
|
@@ -1148,9 +1148,9 @@
|
|
|
1148
1148
|
}
|
|
1149
1149
|
},
|
|
1150
1150
|
"node_modules/@synopackageland/sdk": {
|
|
1151
|
-
"version": "0.9.
|
|
1152
|
-
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.9.
|
|
1153
|
-
"integrity": "sha512-
|
|
1151
|
+
"version": "0.9.1",
|
|
1152
|
+
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.9.1.tgz",
|
|
1153
|
+
"integrity": "sha512-V7V2Ad7KTcQGo3fgMl2PofqhwDvHzCxYJG4q3DqVRuJgNNhxeflZUSYTWHtO/xTB9OZ5KPGoN8hqHIrNDyzAXg==",
|
|
1154
1154
|
"license": "MIT",
|
|
1155
1155
|
"peerDependencies": {
|
|
1156
1156
|
"@lucide/vue": "^1.48.0",
|