@synopackageland/cli 0.10.12 → 0.11.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 +6 -0
- package/README.md +20 -0
- package/dist/add.js +2 -2
- package/dist/deploy/plan.js +1 -1
- package/dist/info.js +3 -3
- package/dist/runtime/executor-build.js +1 -1
- package/dist/runtime/executor-digests.json +4 -136
- package/dist/runtime/executor-policy-check.js +1 -1
- package/dist/runtime/executor-policy.d.ts +3 -0
- package/dist/runtime/executor-policy.js +19 -6
- package/dist/runtime/webapi-profile.d.ts +8 -0
- package/dist/runtime/webapi-profile.js +27 -0
- package/dist/standalone/api-permissions.js +1 -1
- package/dist/standalone/bridge-json.js +1 -0
- package/dist/standalone/dsm-window.js +1 -0
- package/dist/standalone/inspect-spk.js +4 -4
- package/dist/standalone/lifecycle.js +6 -6
- package/dist/standalone/manifest.d.ts +2 -0
- package/dist/standalone/manifest.js +15 -3
- package/dist/standalone/root-window.js +1 -0
- package/dist/standalone/spk.js +1 -1
- package/package.json +2 -2
- package/schema/bridge-manifest.v1.schema.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0 (2026-10-16)
|
|
4
|
+
|
|
5
|
+
- Add capabilities.webapi named profiles with purpose and exact API/method/version lists, pinned in the administrator-approved executor policy. Build, inspect, lifecycle, bridge services and consent details support WebAPI-only Apps without granting file/root operations.
|
|
6
|
+
- Require SDK 0.11.0 for WebAPI profiles and record the three-architecture executor/lifecycle digests. The SDK and templates now share the 0.11 minor line; upgrade backend/frontend pins together.
|
|
7
|
+
- 繁中:新增具名 WebAPI profile,宣告用途與 API/method/version 清單並納入管理員核准;建置、檢查、生命週期與 bridge 支援僅 WebAPI 的 App。使用此功能需 SDK 0.11.0,後端/前端 pin 一併升級至 0.11。
|
|
8
|
+
|
|
3
9
|
## 0.10.12 (2026-10-15)
|
|
4
10
|
|
|
5
11
|
- Generated Apps use templates 0.10.13, SDK 0.10.12 and UI 0.9.10, including PageInfo, responsive page headers and confirmation before removing a Portal page's own entry.
|
package/README.md
CHANGED
|
@@ -184,3 +184,23 @@ Third-party components keep their own terms.
|
|
|
184
184
|
`frontend/` first. A missing notice or version mismatch fails the build.
|
|
185
185
|
- Keep `backend/THIRD_PARTY_NOTICES.txt` current when you change Go modules. Your own binaries,
|
|
186
186
|
added modules, container images and upstream apps need their own notices.
|
|
187
|
+
|
|
188
|
+
SDK 0.11.0 adds delegated JSON WebAPI profiles. With CLI 0.11.0, declare:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
capabilities:
|
|
192
|
+
webapi:
|
|
193
|
+
calendar:
|
|
194
|
+
purpose: Create calendar reminders after workflow approval
|
|
195
|
+
apis:
|
|
196
|
+
SYNO.Cal.Event:
|
|
197
|
+
list: [6]
|
|
198
|
+
create: [6]
|
|
199
|
+
delete: [6]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Use matching backend/frontend SDK 0.11.0 pins. The profile participates in the
|
|
203
|
+
normal executor approval digest; a profile change needs administrator approval.
|
|
204
|
+
Users grant the named profile separately. The permanent denylist, current-user
|
|
205
|
+
permission checks, bounded JSON and uncertain-outcome rules are in the SDK's
|
|
206
|
+
executor/README.md. Binary downloads require later SDK support.
|
package/dist/add.js
CHANGED
|
@@ -71,8 +71,8 @@ function refuseNoPrivilegePortal(root, rest) {
|
|
|
71
71
|
const loaded = loadStandaloneManifest(root);
|
|
72
72
|
if (!loaded.ok)
|
|
73
73
|
throw new Error(loaded.errors.map(e => `[${e.code}] ${e.message}`).join('\n'));
|
|
74
|
-
const { rootOps, delegate } = loaded.manifest.capabilities;
|
|
75
|
-
if (!rootOps && !delegate && !rest.some(name => name === 'root-ops' || name === 'delegate')) {
|
|
74
|
+
const { rootOps, delegate, webapi } = loaded.manifest.capabilities;
|
|
75
|
+
if (!rootOps && !delegate && !webapi && !rest.some(name => name === 'root-ops' || name === 'delegate')) {
|
|
76
76
|
throw new Error(`[STANDALONE_PORTAL_REQUIRES_EXECUTOR] ${PORTAL_REQUIRES_EXECUTOR}`);
|
|
77
77
|
}
|
|
78
78
|
}
|
package/dist/deploy/plan.js
CHANGED
|
@@ -35,7 +35,7 @@ export function planDeployment(root, options = {}) {
|
|
|
35
35
|
arch: options.arch,
|
|
36
36
|
capabilities: Object.keys(m.capabilities),
|
|
37
37
|
transport: "dsm-local-spk",
|
|
38
|
-
notes: m.capabilities.rootOps || m.capabilities.delegate
|
|
38
|
+
notes: m.capabilities.rootOps || m.capabilities.delegate || m.capabilities.webapi
|
|
39
39
|
? [
|
|
40
40
|
"Installation does not mean ready. Approve boot and control tasks once in the App window. An executor-only upgrade stays ready without approval (INFO anchors the executor digest); a policy digest change requires approval again.",
|
|
41
41
|
]
|
package/dist/info.js
CHANGED
|
@@ -140,7 +140,7 @@ export async function standaloneInfo(options) {
|
|
|
140
140
|
const installed = state.installed;
|
|
141
141
|
const capabilities = [];
|
|
142
142
|
let executor;
|
|
143
|
-
for (const name of ["files", "jobs", "identity", "rootOps", "delegate"]) {
|
|
143
|
+
for (const name of ["files", "jobs", "identity", "rootOps", "delegate", "webapi"]) {
|
|
144
144
|
if (!m.capabilities[name])
|
|
145
145
|
continue;
|
|
146
146
|
if (!installed)
|
|
@@ -149,7 +149,7 @@ export async function standaloneInfo(options) {
|
|
|
149
149
|
state: "blocked",
|
|
150
150
|
reason: "Package is not installed; deploy first.",
|
|
151
151
|
});
|
|
152
|
-
else if (name === "rootOps" || name === "delegate") {
|
|
152
|
+
else if (name === "rootOps" || name === "delegate" || name === "webapi") {
|
|
153
153
|
const result = await rootCapability(m, options.host, remote?.version ?? (await options.host.getInstalledPackageVersion?.(m.package)));
|
|
154
154
|
executor = result.executor;
|
|
155
155
|
const { executor: ignored, ...capability } = result;
|
|
@@ -170,7 +170,7 @@ export async function standaloneInfo(options) {
|
|
|
170
170
|
...state,
|
|
171
171
|
capabilities,
|
|
172
172
|
...(executor ? { executor } : {}),
|
|
173
|
-
uninstallCleanup: uninstallCleanup(m.package, { executor: Boolean(m.capabilities.rootOps || m.capabilities.delegate), portal: Boolean(m.portal && !portalByExecutor(m.os_min_ver)), portalEntry: Boolean(m.portal && portalByExecutor(m.os_min_ver)) }),
|
|
173
|
+
uninstallCleanup: uninstallCleanup(m.package, { executor: Boolean(m.capabilities.rootOps || m.capabilities.delegate || m.capabilities.webapi), portal: Boolean(m.portal && !portalByExecutor(m.os_min_ver)), portalEntry: Boolean(m.portal && portalByExecutor(m.os_min_ver)) }),
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
/** What the installed App's portal.status says; DSM owns the alias, an administrator sets it. */
|
|
@@ -23,7 +23,7 @@ function failure(error) {
|
|
|
23
23
|
* the module directory, `go build .` and `./cmd/lifecycle` inside it.
|
|
24
24
|
*/
|
|
25
25
|
export async function buildExecutor(m, arch, work, run, options = {}) {
|
|
26
|
-
if (!m.capabilities.delegate && !m.capabilities.rootOps)
|
|
26
|
+
if (!m.capabilities.delegate && !m.capabilities.rootOps && !m.capabilities.webapi)
|
|
27
27
|
return undefined;
|
|
28
28
|
let sdk;
|
|
29
29
|
try {
|
|
@@ -1,140 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"0.
|
|
3
|
-
"x86_64": "
|
|
4
|
-
"aarch64": "
|
|
5
|
-
"armv7": "
|
|
6
|
-
"lifecycle": {
|
|
7
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
8
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
9
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
10
|
-
},
|
|
11
|
-
"goToolchain": "go1.26.9"
|
|
12
|
-
},
|
|
13
|
-
"0.10.1": {
|
|
14
|
-
"x86_64": "51b538e6ec1cb8fa2a4e00291eda097969352d6e2d9a3e7ade5da5359f1065de",
|
|
15
|
-
"aarch64": "9038b5ad7b4822e967f18390ca28b6f0021a3044ed461809b3f7333f17400840",
|
|
16
|
-
"armv7": "22d872cc618e3af4a052955186d36a6851e9f96ad959ee8ea7d3fbb5eec968bd",
|
|
17
|
-
"lifecycle": {
|
|
18
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
19
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
20
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
21
|
-
},
|
|
22
|
-
"goToolchain": "go1.26.9"
|
|
23
|
-
},
|
|
24
|
-
"0.10.2": {
|
|
25
|
-
"x86_64": "bb7aebb120562633e852d590007870489b7839ea2c943db897b7a71dea743fae",
|
|
26
|
-
"aarch64": "2fd8365aabfa2ea2ea7c767f89fa8222d7f4fc04cbdde6b7d510c74002a5bb89",
|
|
27
|
-
"armv7": "6f757614eab802fa07c2601c801bcf2c14ad3076d7c01f9ed20d62744ef13979",
|
|
28
|
-
"lifecycle": {
|
|
29
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
30
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
31
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
32
|
-
},
|
|
33
|
-
"goToolchain": "go1.26.9"
|
|
34
|
-
},
|
|
35
|
-
"0.10.3": {
|
|
36
|
-
"x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
|
|
37
|
-
"aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
|
|
38
|
-
"armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
|
|
39
|
-
"lifecycle": {
|
|
40
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
41
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
42
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
43
|
-
},
|
|
44
|
-
"goToolchain": "go1.26.9"
|
|
45
|
-
},
|
|
46
|
-
"0.10.4": {
|
|
47
|
-
"x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
|
|
48
|
-
"aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
|
|
49
|
-
"armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
|
|
50
|
-
"lifecycle": {
|
|
51
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
52
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
53
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
54
|
-
},
|
|
55
|
-
"goToolchain": "go1.26.9"
|
|
56
|
-
},
|
|
57
|
-
"0.10.5": {
|
|
58
|
-
"x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
|
|
59
|
-
"aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
|
|
60
|
-
"armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
|
|
61
|
-
"lifecycle": {
|
|
62
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
63
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
64
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
65
|
-
},
|
|
66
|
-
"goToolchain": "go1.26.9"
|
|
67
|
-
},
|
|
68
|
-
"0.10.6": {
|
|
69
|
-
"x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
|
|
70
|
-
"aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
|
|
71
|
-
"armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
|
|
72
|
-
"lifecycle": {
|
|
73
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
74
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
75
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
76
|
-
},
|
|
77
|
-
"goToolchain": "go1.26.9"
|
|
78
|
-
},
|
|
79
|
-
"0.10.7": {
|
|
80
|
-
"x86_64": "4ba7040761a719e74d6b047ef68b9415e658ebb8b373b69555b8c307524e26ba",
|
|
81
|
-
"aarch64": "ba80b389e3291419389b66be7b0a0e1085d3dfec55c9c651fecae427279bb3d5",
|
|
82
|
-
"armv7": "49474467b9c9a7f31b30b5053b70eb4f71a95f2c06fbf3566e8de8f16972a299",
|
|
83
|
-
"lifecycle": {
|
|
84
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
85
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
86
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
87
|
-
},
|
|
88
|
-
"goToolchain": "go1.26.9"
|
|
89
|
-
},
|
|
90
|
-
"0.10.8": {
|
|
91
|
-
"x86_64": "ba38f5764d5f0510706afe39979eef18bf2b8a72421a3a495810c628db0c962d",
|
|
92
|
-
"aarch64": "b5129b26714c25c0eda83931f3a66961fa9ecd059074c534159db6a68aefd8f7",
|
|
93
|
-
"armv7": "359005fbab7714c29d0d0598f09f3b011aa643be26d0368726a023edbef53b8e",
|
|
94
|
-
"lifecycle": {
|
|
95
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
96
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
97
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
98
|
-
},
|
|
99
|
-
"goToolchain": "go1.26.9"
|
|
100
|
-
},
|
|
101
|
-
"0.10.9": {
|
|
102
|
-
"x86_64": "ba38f5764d5f0510706afe39979eef18bf2b8a72421a3a495810c628db0c962d",
|
|
103
|
-
"aarch64": "b5129b26714c25c0eda83931f3a66961fa9ecd059074c534159db6a68aefd8f7",
|
|
104
|
-
"armv7": "359005fbab7714c29d0d0598f09f3b011aa643be26d0368726a023edbef53b8e",
|
|
105
|
-
"lifecycle": {
|
|
106
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
107
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
108
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
109
|
-
},
|
|
110
|
-
"goToolchain": "go1.26.9"
|
|
111
|
-
},
|
|
112
|
-
"0.10.10": {
|
|
113
|
-
"x86_64": "9c2c6f92edd2536b3061f8b9eef8b500a1f064909deb4c1b44b496465879b90d",
|
|
114
|
-
"aarch64": "175731dafb52b83f771878cd41407cae6353a90628649ec78118d6a1e1ffb9fb",
|
|
115
|
-
"armv7": "27402fa2aa47732a280434cefaa876b0cfe8202a6dc103fd9dd1984e9dc69c81",
|
|
116
|
-
"lifecycle": {
|
|
117
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
118
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
119
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
120
|
-
},
|
|
121
|
-
"goToolchain": "go1.26.9"
|
|
122
|
-
},
|
|
123
|
-
"0.10.11": {
|
|
124
|
-
"x86_64": "7fe3118a8de76b2d83ff4410ce76dd1aca791388286a0929602beaf4536a7194",
|
|
125
|
-
"aarch64": "0d44c4da7c464d62fcdba8d0ea5f65b935fc505e0faaa8bd61bbb5ec4deb1cea",
|
|
126
|
-
"armv7": "720a321c56669f31bc289e303627b9b88a56b9ca195ce837821639479491ab8f",
|
|
127
|
-
"lifecycle": {
|
|
128
|
-
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
129
|
-
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
130
|
-
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
131
|
-
},
|
|
132
|
-
"goToolchain": "go1.26.9"
|
|
133
|
-
},
|
|
134
|
-
"0.10.12": {
|
|
135
|
-
"x86_64": "7fe3118a8de76b2d83ff4410ce76dd1aca791388286a0929602beaf4536a7194",
|
|
136
|
-
"aarch64": "0d44c4da7c464d62fcdba8d0ea5f65b935fc505e0faaa8bd61bbb5ec4deb1cea",
|
|
137
|
-
"armv7": "720a321c56669f31bc289e303627b9b88a56b9ca195ce837821639479491ab8f",
|
|
2
|
+
"0.11.0": {
|
|
3
|
+
"x86_64": "abe0a42699fed06aeeb7520e5a4f3393cae64f83a66773b70630f0db0c5ed107",
|
|
4
|
+
"aarch64": "1fdf171a665279f16ca1c2605a54996b2359fa69531c6fe4e890f5bda61c1bef",
|
|
5
|
+
"armv7": "946f73df833929283f94289f025fd73897416777be5e0867d1e73f0b6c3031d7",
|
|
138
6
|
"lifecycle": {
|
|
139
7
|
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
140
8
|
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
@@ -53,7 +53,7 @@ export function executorPolicyProblem(executorDir, sdkVersion, pkg, policyText,
|
|
|
53
53
|
}
|
|
54
54
|
/** The policy of each architecture `check` would build, checked by the App's SDK executor; Apps without executor need none. */
|
|
55
55
|
export function manifestPolicyProblem(m, env = process.env) {
|
|
56
|
-
if (!m.capabilities.delegate && !m.capabilities.rootOps)
|
|
56
|
+
if (!m.capabilities.delegate && !m.capabilities.rootOps && !m.capabilities.webapi)
|
|
57
57
|
return undefined;
|
|
58
58
|
try {
|
|
59
59
|
const sdk = requiredSdk(sdkBackend(m), env);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WebAPIProfiles } from './webapi-profile.js';
|
|
1
2
|
import type { RootOp } from '../standalone/root-ops.js';
|
|
2
3
|
import type { EntryMode } from '../standalone/manifest.js';
|
|
3
4
|
import { type PortalDeclaration, type PortalEntry } from '../portal/alias.js';
|
|
@@ -17,6 +18,7 @@ export interface DelegateCapability {
|
|
|
17
18
|
export declare function validDelegate(value: unknown): value is DelegateCapability;
|
|
18
19
|
export declare const sha256: (data: string | Buffer) => string;
|
|
19
20
|
export interface ExecutorPolicy {
|
|
21
|
+
webapi?: WebAPIProfiles;
|
|
20
22
|
v: 1;
|
|
21
23
|
app: string;
|
|
22
24
|
/** Present only with a manifest `entry`; the executor then applies the same App Privileges rule offline. */
|
|
@@ -47,6 +49,7 @@ export declare function executorPolicy(m: {
|
|
|
47
49
|
capabilities: {
|
|
48
50
|
delegate?: DelegateCapability;
|
|
49
51
|
rootOps?: true;
|
|
52
|
+
webapi?: WebAPIProfiles;
|
|
50
53
|
};
|
|
51
54
|
rootOps?: RootOp[];
|
|
52
55
|
rootPrograms?: RootSettings;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validWebAPIProfiles, sortedWebAPIProfiles } from './webapi-profile.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { MOVE_FROM_STAGING, DELEGATE_EXTENSION_OPERATIONS } from './delegate-contract.js';
|
|
3
4
|
import { portalByExecutor } from '../build-spk/dsm-version.js';
|
|
@@ -29,6 +30,8 @@ export const executorPolicyText = (policy) => JSON.stringify(policy) + '\n';
|
|
|
29
30
|
export function executorPolicy(m, arch) {
|
|
30
31
|
if (m.capabilities.delegate && !validDelegate(m.capabilities.delegate))
|
|
31
32
|
throw new Error('[EXECUTOR_POLICY] Invalid delegate operations or scope.');
|
|
33
|
+
if (m.capabilities.webapi && !validWebAPIProfiles(m.capabilities.webapi))
|
|
34
|
+
throw new Error('[EXECUTOR_POLICY] Invalid WebAPI profiles.');
|
|
32
35
|
const portal = executorPortal(m);
|
|
33
36
|
const operations = ['status', ...(portal ? PORTAL_OPERATIONS : [])];
|
|
34
37
|
if (m.capabilities.delegate) {
|
|
@@ -38,6 +41,8 @@ export function executorPolicy(m, arch) {
|
|
|
38
41
|
if (m.capabilities.delegate.lifetime === 'until-revoked')
|
|
39
42
|
operations.push(...DELEGATE_EXTENSION_OPERATIONS);
|
|
40
43
|
}
|
|
44
|
+
if (m.capabilities.webapi)
|
|
45
|
+
operations.push('delegate.create', 'delegate.list', 'delegate.revoke', 'delegate.disable', 'delegate.enable', 'delegate.audit', 'webapi.call');
|
|
41
46
|
const root = {};
|
|
42
47
|
const settings = m.capabilities.rootOps ? m.rootPrograms : undefined;
|
|
43
48
|
if (m.capabilities.rootOps) {
|
|
@@ -51,6 +56,7 @@ export function executorPolicy(m, arch) {
|
|
|
51
56
|
...(d?.lifetime ? { destinations: [...d.destinations].sort(), lifetime: d.lifetime } : {}),
|
|
52
57
|
operations: [...new Set(operations)].sort(), ...(portal ? { portal } : {}), ...(purge ? { purge } : {}), ...(settings?.stop ? { stop: settings.stop } : {}),
|
|
53
58
|
...(settings?.start ? { start: settings.start } : {}),
|
|
59
|
+
...(m.capabilities.webapi ? { webapi: sortedWebAPIProfiles(m.capabilities.webapi) } : {}),
|
|
54
60
|
root, ...(settings?.state ? { root_state: true } : {}) };
|
|
55
61
|
}
|
|
56
62
|
/** A script without settings keeps the bare digest; a binary pins the bytes built for this architecture. */
|
|
@@ -80,7 +86,7 @@ export function validExecutorPolicy(value, pkg) {
|
|
|
80
86
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
81
87
|
return false;
|
|
82
88
|
const p = value;
|
|
83
|
-
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'stop', 'start', 'root', 'root_state'].includes(key))
|
|
89
|
+
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'stop', 'start', 'root', 'root_state', 'webapi'].includes(key))
|
|
84
90
|
|| p.v !== 1 || p.package !== pkg || p.app !== `SYNO.SDS.${pkg}.Instance`
|
|
85
91
|
|| 'entry' in p && p.entry !== 'admin-only' && p.entry !== 'app-privilege'
|
|
86
92
|
|| !Array.isArray(p.operations) || !p.operations.includes('status') || new Set(p.operations).size !== p.operations.length
|
|
@@ -95,18 +101,25 @@ export function validExecutorPolicy(value, pkg) {
|
|
|
95
101
|
if (![...ROOT_JOB_OPERATIONS, 'root.run'].every(op => p.operations.includes(op) === rootOps.includes(op)))
|
|
96
102
|
return false;
|
|
97
103
|
const base = ['status', 'root.run', ...ROOT_JOB_OPERATIONS, 'delegate.create', 'delegate.list', 'delegate.revoke'];
|
|
98
|
-
if (p.operations.some(op => ![...base, ...FILE_OPERATIONS, ...FILE_TASK_OPERATIONS, ...DELEGATE_EXTENSION_OPERATIONS, ...PORTAL_OPERATIONS].includes(op)))
|
|
104
|
+
if (p.operations.some(op => ![...base, ...FILE_OPERATIONS, ...FILE_TASK_OPERATIONS, ...DELEGATE_EXTENSION_OPERATIONS, ...PORTAL_OPERATIONS, 'webapi.call'].includes(op)))
|
|
99
105
|
return false;
|
|
100
106
|
if (!PORTAL_OPERATIONS.every(op => p.operations.includes(op) === (p.portal !== undefined)) || p.portal !== undefined && !validPortalEntry(p.portal, pkg))
|
|
101
107
|
return false;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
if ((p.webapi !== undefined) !== p.operations.includes('webapi.call') || p.webapi !== undefined && !validWebAPIProfiles(p.webapi))
|
|
109
|
+
return false;
|
|
110
|
+
const delegated = p.operations.some(op => op.startsWith('files.'));
|
|
111
|
+
if (!delegated) {
|
|
112
|
+
if (p.scope !== undefined || p.lifetime !== undefined || p.destinations !== undefined)
|
|
113
|
+
return false;
|
|
114
|
+
const expected = executorPolicy({ package: pkg, capabilities: { ...(p.webapi ? { webapi: p.webapi } : {}) } }).operations;
|
|
115
|
+
const declared = p.operations.filter(op => !PORTAL_OPERATIONS.includes(op) && !rootOps.includes(op));
|
|
116
|
+
return expected.length === declared.length && expected.every(op => declared.includes(op));
|
|
117
|
+
}
|
|
105
118
|
const capability = { scope: p.scope, operations: p.operations.filter(op => FILE_OPERATIONS.includes(op) || op === MOVE_FROM_STAGING),
|
|
106
119
|
...('lifetime' in p ? { lifetime: p.lifetime } : {}), ...('destinations' in p ? { destinations: p.destinations } : {}) };
|
|
107
120
|
if (!validDelegate(capability))
|
|
108
121
|
return false;
|
|
109
|
-
const expected = [...executorPolicy({ package: pkg, capabilities: { delegate: capability } }).operations, ...rootOps];
|
|
122
|
+
const expected = [...executorPolicy({ package: pkg, capabilities: { delegate: capability, ...(p.webapi ? { webapi: p.webapi } : {}) } }).operations, ...rootOps];
|
|
110
123
|
const declared = p.operations.filter(op => !PORTAL_OPERATIONS.includes(op));
|
|
111
124
|
return expected.length === declared.length && expected.every(op => declared.includes(op));
|
|
112
125
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** JSON WebAPI profiles, independently enforced again by the pinned executor. */
|
|
2
|
+
export interface WebAPIProfile {
|
|
3
|
+
purpose: string;
|
|
4
|
+
apis: Record<string, Record<string, number[]>>;
|
|
5
|
+
}
|
|
6
|
+
export type WebAPIProfiles = Record<string, WebAPIProfile>;
|
|
7
|
+
export declare function validWebAPIProfiles(value: unknown): value is WebAPIProfiles;
|
|
8
|
+
export declare function sortedWebAPIProfiles(profiles: WebAPIProfiles): WebAPIProfiles;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
+
const blocked = new Set(['api', 'entry', 'core', 'auth', 'authentication', 'filestation', 'package', 'packagemanager', 'packagemanagement', 'user', 'users', 'group', 'groups', 'permission', 'permissions', 'privilege', 'privileges', 'owner', 'sharing', 'admin', 'setting', 'settings', 'teamfolder', 'mailplusserver', 'directoryserver', 'taskscheduler', 'eventscheduler', 'request', 'batch', 'compound']);
|
|
3
|
+
const methodBlocked = new Set(['auth', 'login', 'logout', 'sudo', 'permission', 'permissions', 'privilege', 'privileges', 'share', 'sharing', 'owner', 'admin']);
|
|
4
|
+
export function validWebAPIProfiles(value) {
|
|
5
|
+
if (!record(value) || !Object.keys(value).length || Object.keys(value).length > 32)
|
|
6
|
+
return false;
|
|
7
|
+
let total = 0;
|
|
8
|
+
for (const [name, p] of Object.entries(value)) {
|
|
9
|
+
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(name) || name === 'home' || !record(p) || Object.keys(p).some(k => !['purpose', 'apis'].includes(k)) || typeof p.purpose !== 'string' || !p.purpose.trim() || Buffer.byteLength(p.purpose) > 1024 || /[\x00-\x1f\x7f]/.test(p.purpose) || !record(p.apis) || !Object.keys(p.apis).length)
|
|
10
|
+
return false;
|
|
11
|
+
for (const [api, methods] of Object.entries(p.apis)) {
|
|
12
|
+
if (!/^SYNO\.[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)*$/.test(api) || api.toLowerCase().split('.').slice(1).some(x => blocked.has(x)) || !record(methods) || !Object.keys(methods).length)
|
|
13
|
+
return false;
|
|
14
|
+
for (const [method, versions] of Object.entries(methods)) {
|
|
15
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(method) || method.split('_').some(x => methodBlocked.has(x)) || !Array.isArray(versions) || !versions.length || new Set(versions).size !== versions.length || versions.some(x => !Number.isSafeInteger(x) || x < 1 || x > 65535))
|
|
16
|
+
return false;
|
|
17
|
+
total += versions.length;
|
|
18
|
+
if (total > 256)
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
export function sortedWebAPIProfiles(profiles) {
|
|
26
|
+
return Object.fromEntries(Object.entries(profiles).sort(([a], [b]) => a < b ? -1 : 1).map(([name, p]) => [name, { purpose: p.purpose, apis: Object.fromEntries(Object.entries(p.apis).sort(([a], [b]) => a < b ? -1 : 1).map(([api, methods]) => [api, Object.fromEntries(Object.entries(methods).sort(([a], [b]) => a < b ? -1 : 1).map(([m, versions]) => [m, [...versions].sort((a, b) => a - b)]))])) }]));
|
|
27
|
+
}
|
|
@@ -49,7 +49,7 @@ export function readApiPermissions(backend, capabilities = {}) {
|
|
|
49
49
|
throw new Error(`App policy cannot override SDK route ${method}`);
|
|
50
50
|
}
|
|
51
51
|
for (const [capability, methods] of Object.entries(sdk)) {
|
|
52
|
-
if (capabilities[capability] || ['rootOps', 'files'].includes(capability) && capabilities.delegate)
|
|
52
|
+
if (capabilities[capability] || ['rootOps', 'files'].includes(capability) && capabilities.delegate || capability === 'rootOps' && capabilities.webapi)
|
|
53
53
|
Object.assign(policy.methods, methods);
|
|
54
54
|
}
|
|
55
55
|
return { policy, sdk, sdkDigest: createHash("sha256").update(sdkRaw).digest("hex"), digest: createHash("sha256").update(raw).digest("hex") };
|
|
@@ -22,6 +22,7 @@ export function renderBridgeJson(m, methods) {
|
|
|
22
22
|
...(m.portal ? { portal: true } : {}),
|
|
23
23
|
...(capabilities.jobs ? { jobs: true } : {}),
|
|
24
24
|
...(capabilities.rootOps ? { root: true } : {}),
|
|
25
|
+
...(capabilities.webapi ? { webapi: true } : {}),
|
|
25
26
|
...(delegate ? { delegate: {
|
|
26
27
|
scope: delegate.scope,
|
|
27
28
|
destinations: [...delegate.destinations ?? []].sort(),
|
|
@@ -495,6 +495,7 @@ ${PASSWORD_CONFIRM}
|
|
|
495
495
|
};
|
|
496
496
|
var access = ${ns}.createRootAccess({
|
|
497
497
|
tasks: ${ns}.rootTasks,
|
|
498
|
+
webapiProfiles: ${JSON.stringify(manifest.capabilities.webapi ?? {})},
|
|
498
499
|
pins: ${JSON.stringify({ package: manifest.package, executorDigest: manifest.executor.executorDigest, policyDigest: manifest.executor.policyDigest })},
|
|
499
500
|
user: session,
|
|
500
501
|
send: ${ns}.webApi(me),
|
|
@@ -62,7 +62,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
62
62
|
if (metadataText !== undefined) {
|
|
63
63
|
try {
|
|
64
64
|
const raw = JSON.parse(metadataText);
|
|
65
|
-
if (raw.schema !== 2 || !["go", "python", "node", "rust", "binary"].includes(raw.backend) || !Array.isArray(raw.capabilities) || raw.capabilities.some((name) => !["files", "jobs", "identity", "rootOps", "delegate"].includes(String(name))) || new Set(raw.capabilities).size !== raw.capabilities.length || (raw.entry !== undefined && !["admin-only", "app-privilege"].includes(raw.entry)))
|
|
65
|
+
if (raw.schema !== 2 || !["go", "python", "node", "rust", "binary"].includes(raw.backend) || !Array.isArray(raw.capabilities) || raw.capabilities.some((name) => !["files", "jobs", "identity", "rootOps", "delegate", "webapi"].includes(String(name))) || new Set(raw.capabilities).size !== raw.capabilities.length || (raw.entry !== undefined && !["admin-only", "app-privilege"].includes(raw.entry)))
|
|
66
66
|
throw new Error();
|
|
67
67
|
metadata = raw;
|
|
68
68
|
}
|
|
@@ -96,14 +96,14 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
96
96
|
if (kind === "container")
|
|
97
97
|
await checkContainerExposure(dir, join(dir, "package.tgz"), findings);
|
|
98
98
|
const bridge = compiledKind && kind !== "native-go" ? await checkBridgeApp(dir, metadata.backend, info, payload, findings) : undefined;
|
|
99
|
-
const hasExecutor = (metadata.capabilities ?? []).some(name => name === "rootOps" || name === "delegate") || paths.has("executor/executor");
|
|
99
|
+
const hasExecutor = (metadata.capabilities ?? []).some(name => name === "rootOps" || name === "delegate" || name === "webapi") || paths.has("executor/executor");
|
|
100
100
|
if (standalone && portal && !hasExecutor)
|
|
101
101
|
findings.push("PORTAL_REQUIRES_EXECUTOR portal needs the executor level (rootOps or delegate): after uninstall only a root purge can stop nginx serving the alias");
|
|
102
102
|
const daemon = standalone ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background) : undefined;
|
|
103
103
|
if (background)
|
|
104
104
|
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go");
|
|
105
105
|
let executor;
|
|
106
|
-
if ((metadata.capabilities ?? []).some(name => name === "rootOps" || name === "delegate") || paths.has("executor/executor")) {
|
|
106
|
+
if ((metadata.capabilities ?? []).some(name => name === "rootOps" || name === "delegate" || name === "webapi") || paths.has("executor/executor")) {
|
|
107
107
|
const executorDir = join(dir, "executor-inspection");
|
|
108
108
|
mkdirSync(executorDir);
|
|
109
109
|
await tar.x({ file: join(dir, "package.tgz"), cwd: executorDir });
|
|
@@ -168,7 +168,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
168
168
|
cgi: cgiEntry?.mode === "0755" ? CGI_PAYLOAD_PATH : undefined,
|
|
169
169
|
payload,
|
|
170
170
|
findings,
|
|
171
|
-
capabilities: (metadata.capabilities ?? []).map(name => ({ name, requiresAdminApproval: name === "rootOps" || name === "delegate" })),
|
|
171
|
+
capabilities: (metadata.capabilities ?? []).map(name => ({ name, requiresAdminApproval: name === "rootOps" || name === "delegate" || name === "webapi" })),
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
finally {
|
|
@@ -15,7 +15,7 @@ export function renderStandaloneScript(name, manifest) {
|
|
|
15
15
|
return manifest.appScripts?.some(app => app === name) ? composeAppScript(name, sdk) : sdk;
|
|
16
16
|
}
|
|
17
17
|
function renderCapabilityScript(name, manifest) {
|
|
18
|
-
if (name === 'preupgrade' && (manifest.capabilities.rootOps || manifest.capabilities.delegate)) {
|
|
18
|
+
if (name === 'preupgrade' && (manifest.capabilities.rootOps || manifest.capabilities.delegate || manifest.capabilities.webapi)) {
|
|
19
19
|
const installed = `\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}`;
|
|
20
20
|
// Incoming preupgrade sees the OLD payload. Only complete absence is first
|
|
21
21
|
// introduction; a partial or symlinked executor must still fail closed.
|
|
@@ -31,10 +31,10 @@ function renderStandaloneScriptBody(name, manifest) {
|
|
|
31
31
|
const normal = renderCapabilityScript(name, { ...manifest, capabilities: { ...manifest.capabilities, delegate: { ...manifest.capabilities.delegate, scope: 'home' } } });
|
|
32
32
|
return normal.replace('#!/bin/sh\n', () => '#!/bin/sh\n' + renderDelegateInstallDraft(manifest.package, name === 'preinst'));
|
|
33
33
|
}
|
|
34
|
-
if (manifest.daemon && manifest.capabilities?.jobs && (manifest.capabilities.delegate || manifest.capabilities.rootOps)
|
|
34
|
+
if (manifest.daemon && manifest.capabilities?.jobs && (manifest.capabilities.delegate || manifest.capabilities.rootOps || manifest.capabilities.webapi)
|
|
35
35
|
&& ['start-stop-status', 'preupgrade', 'preuninst'].includes(name))
|
|
36
36
|
return renderDelegatedDaemonScript(name, manifest);
|
|
37
|
-
if ((manifest.capabilities?.rootOps || manifest.capabilities?.delegate) && ["preuninst", "preupgrade", "start-stop-status"].includes(name)) {
|
|
37
|
+
if ((manifest.capabilities?.rootOps || manifest.capabilities?.delegate || manifest.capabilities?.webapi) && ["preuninst", "preupgrade", "start-stop-status"].includes(name)) {
|
|
38
38
|
const helper = `"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/executor/lifecycle"`;
|
|
39
39
|
// Attempt worker stop first, but let executor shutdown drain requests and
|
|
40
40
|
// close ingress even when it fails; a still-running worker fails the hook.
|
|
@@ -43,7 +43,7 @@ function renderStandaloneScriptBody(name, manifest) {
|
|
|
43
43
|
const workerResult = manifest.capabilities.jobs ? 'exit "$worker_result"\n' : "exit 0\n";
|
|
44
44
|
const request = (action) => `${helper} ${action} --package ${manifest.package} || exit $?\n`;
|
|
45
45
|
if (name === "start-stop-status") {
|
|
46
|
-
const normal = renderCapabilityScript(name, { ...manifest, capabilities: { ...manifest.capabilities, rootOps: undefined, delegate: undefined } });
|
|
46
|
+
const normal = renderCapabilityScript(name, { ...manifest, capabilities: { ...manifest.capabilities, rootOps: undefined, delegate: undefined, webapi: undefined } });
|
|
47
47
|
const stopDaemon = manifest.daemon ? `"/var/packages/${manifest.package}/target/daemon/control" stop "\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${manifest.package}/var}" || exit $?\n` : "";
|
|
48
48
|
return normal.replace("#!/bin/sh\n", `#!/bin/sh\n${workerStop}if [ "$1" = start ]; then\n${request("start")}fi\nif [ "$1" = stop ]; then\n${stopWorker}${stopDaemon}${request("stop")}${workerResult}fi\n`);
|
|
49
49
|
}
|
|
@@ -57,8 +57,8 @@ function renderStandaloneScriptBody(name, manifest) {
|
|
|
57
57
|
const body = manifest.daemon ? renderDaemonPreuninst(manifest) : `${UNINSTALL_ONLY}exit 0\n`;
|
|
58
58
|
return `#!/bin/sh\n${renderUninstallHook(manifest)}${body}`;
|
|
59
59
|
}
|
|
60
|
-
if ((manifest.capabilities?.rootOps || manifest.capabilities?.delegate) && ["postinst", "postupgrade"].includes(name)) {
|
|
61
|
-
const normal = renderCapabilityScript(name, { ...manifest, capabilities: { ...manifest.capabilities, rootOps: undefined, delegate: undefined } });
|
|
60
|
+
if ((manifest.capabilities?.rootOps || manifest.capabilities?.delegate || manifest.capabilities?.webapi) && ["postinst", "postupgrade"].includes(name)) {
|
|
61
|
+
const normal = renderCapabilityScript(name, { ...manifest, capabilities: { ...manifest.capabilities, rootOps: undefined, delegate: undefined, webapi: undefined } });
|
|
62
62
|
return normal.replace("#!/bin/sh\n", `#!/bin/sh\n"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/executor/lifecycle" start --package ${manifest.package} || exit $?\n`);
|
|
63
63
|
}
|
|
64
64
|
if (manifest.sharedFolder && (name === "postinst" || name === "postupgrade"))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WebAPIProfiles } from '../runtime/webapi-profile.js';
|
|
1
2
|
import { type PackageMetadata } from "../package-metadata.js";
|
|
2
3
|
import { type DelegateCapability } from "../runtime/executor-policy.js";
|
|
3
4
|
import type { ExecutorPins, ExecutorTask } from "../runtime/executor-tasks.js";
|
|
@@ -96,6 +97,7 @@ export interface StandaloneManifest {
|
|
|
96
97
|
*/
|
|
97
98
|
export type EntryMode = "admin-only" | "app-privilege";
|
|
98
99
|
export interface Capabilities {
|
|
100
|
+
webapi?: WebAPIProfiles;
|
|
99
101
|
files?: "session";
|
|
100
102
|
identity?: "standalone";
|
|
101
103
|
jobs?: Background;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validWebAPIProfiles } from '../runtime/webapi-profile.js';
|
|
1
2
|
import { validateMetadata } from "../package-metadata.js";
|
|
2
3
|
import { validDelegate } from "../runtime/executor-policy.js";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -207,16 +208,21 @@ function readManifest(raw, c) {
|
|
|
207
208
|
if (capabilities.delegate?.scope === "install-selected" && ui && !ui.build)
|
|
208
209
|
c.fail("CAPABILITY_COMBINATION_INVALID", "The delegate install-selected scope can only be confirmed in a Vue panel; plain UI does not support it, use a vue UI (init --ui vue).");
|
|
209
210
|
if (backend && backend.kind !== "binary") {
|
|
210
|
-
for (const problem of sdkProblems(backend, ui?.build?.cwd, isBridgeBackend(backend) || Boolean(capabilities.delegate || capabilities.rootOps)))
|
|
211
|
+
for (const problem of sdkProblems(backend, ui?.build?.cwd, isBridgeBackend(backend) || Boolean(capabilities.delegate || capabilities.rootOps || capabilities.webapi)))
|
|
211
212
|
c.fail(problem.code, problem.message);
|
|
212
213
|
}
|
|
213
214
|
const i18n = raw.i18n === undefined ? undefined : readI18n(raw.i18n, c);
|
|
214
215
|
const icon = raw.icon === undefined ? undefined : readIcon(raw.icon, c);
|
|
216
|
+
if (capabilities.webapi && backend) {
|
|
217
|
+
const sdk = sdkVersionOrUndefined(backend);
|
|
218
|
+
if (!sdk || Number(sdk.split('.')[0]) === 0 && Number(sdk.split('.')[1]) < 11)
|
|
219
|
+
c.fail("SDK_UNSUPPORTED", "capabilities.webapi requires SDK 0.11.0 or later; update backend and frontend together.");
|
|
220
|
+
}
|
|
215
221
|
const uninstall = readUninstall(raw.uninstall, { rootOps: capabilities.rootOps === true, ops: rootOps, en: i18n?.catalogs?.en }, e => c.fail(e.code, e.message));
|
|
216
222
|
const appScripts = readAppScripts(c.root, e => c.fail(e.code, e.message));
|
|
217
223
|
const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
|
|
218
224
|
?? c.fail("STANDALONE_PORTAL_INVALID", "portal must be {} or { alias: <1-64 lowercase alphanumerics or -, not a DSM reserved path> }.");
|
|
219
|
-
if (portal && !capabilities.rootOps && !capabilities.delegate) {
|
|
225
|
+
if (portal && !capabilities.rootOps && !capabilities.delegate && !capabilities.webapi) {
|
|
220
226
|
c.fail("STANDALONE_PORTAL_REQUIRES_EXECUTOR", PORTAL_REQUIRES_EXECUTOR);
|
|
221
227
|
}
|
|
222
228
|
if (portal && osMinVer && !isDsmAtLeast(osMinVer, PORTAL_EXECUTOR_MIN_DSM)) {
|
|
@@ -538,7 +544,13 @@ function readCapabilities(value, c) {
|
|
|
538
544
|
}
|
|
539
545
|
const result = {};
|
|
540
546
|
for (const [name, setting] of Object.entries(value)) {
|
|
541
|
-
if (name === "
|
|
547
|
+
if (name === "webapi") {
|
|
548
|
+
if (validWebAPIProfiles(setting))
|
|
549
|
+
result.webapi = setting;
|
|
550
|
+
else
|
|
551
|
+
c.fail("CAPABILITY_INVALID", "webapi requires named profiles with purpose and exact safe API/method/version lists.");
|
|
552
|
+
}
|
|
553
|
+
else if (name === "delegate") {
|
|
542
554
|
if (validDelegate(setting))
|
|
543
555
|
result.delegate = setting;
|
|
544
556
|
else
|
|
@@ -367,6 +367,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
367
367
|
}, verify);
|
|
368
368
|
}
|
|
369
369
|
return {
|
|
370
|
+
webapiProfiles: options.webapiProfiles || {},
|
|
370
371
|
status: function () { return (isAdmin() ? adminStatus() : userStatus()).then(publish); },
|
|
371
372
|
authorize: function (password, selectedScope) {
|
|
372
373
|
if (!busy) busy = Promise.resolve().then(function () { return approve(password, selectedScope); }).catch(function (e) { var r = block({tasks: {}}, e); r.retry = !!(e && e.passwordConfirm); return r; }).then(function (r) { busy = null; return publish(r); });
|
package/dist/standalone/spk.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
60
60
|
const { manifest } = options;
|
|
61
61
|
const staging = mkdtempSync(join(tmpdir(), "syno-standalone-spk-"));
|
|
62
62
|
try {
|
|
63
|
-
if ((manifest.capabilities.rootOps || manifest.capabilities.delegate) && !options.executor)
|
|
63
|
+
if ((manifest.capabilities.rootOps || manifest.capabilities.delegate || manifest.capabilities.webapi) && !options.executor)
|
|
64
64
|
throw new StandaloneBuildError("EXECUTOR_PIN_REQUIRED", "Executor artifact required.");
|
|
65
65
|
const icons = packageIcons(manifest.icon);
|
|
66
66
|
const uiRoot = join(staging, "package", "ui");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@synopackageland/templates": "0.
|
|
28
|
+
"@synopackageland/templates": "0.11.0",
|
|
29
29
|
"ajv": "^8.17.1",
|
|
30
30
|
"commander": "^13.1.0",
|
|
31
31
|
"tar": "^7.4.3",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"identity": { "const": "standalone", "$comment": "The SDK's App sign-in (identity.*) authenticates instead of DSM." },
|
|
60
60
|
"portal": { "type": "boolean", "$comment": "true serves portal.status, to the browser and as the svc portal.status." },
|
|
61
61
|
"jobs": { "type": "boolean", "$comment": "Added in v1 (additive). true offers the svc bg.submit, bg.get and bg.cancel on the package's job store." },
|
|
62
|
+
"webapi": { "type": "boolean", "$comment": "Since 0.11.0: profile grant/list/revoke and JSON call services through the executor." },
|
|
62
63
|
"root": { "type": "boolean", "$comment": "Added in v1 (additive). true offers the svc root.run, root.start, root.jobs.list, root.jobs.get and root.jobs.stop: the runtime forwards them to the package's executor with the request's DSM session, and the executor's policy and the App privilege decide. It also serves the browser routes executor.status and executor.control (README, Executor routes)." },
|
|
63
64
|
"delegate": {
|
|
64
65
|
"type": "object",
|