@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 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
  }
@@ -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.10.0": {
3
- "x86_64": "fb2df6e3421361c73a1cd720ec12198e968f0d2a58b18b53151c7781c7f4c1ef",
4
- "aarch64": "0a0f6d013c0174f3ea54585182ddda14c94064f331189ac4315de813099efcdd",
5
- "armv7": "3fbb5bc74a03a9767ac06a389a10ff07a353c4c56be6fda7b7555a6759992aba",
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
- const delegated = p.operations.some(op => op.startsWith('files.') || op.startsWith('delegate.'));
103
- if (!delegated)
104
- return p.scope === undefined && p.lifetime === undefined && p.destinations === undefined;
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 === "delegate") {
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); });
@@ -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.10.12",
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.10.13",
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",