@synopackageland/cli 0.15.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.1 (2026-10-18)
4
+
5
+ - Require explicit `DSM_E2E_SSH` for e2e tunnels and use `dsm` as the default saved-profile target; set `DSM_E2E_TARGET` to reuse an earlier target name. Anonymize internal NAS fixtures and documentation.
6
+ - Pin templates and SDK fixtures/examples to 0.15.1. Add the 0.15.1 executor/lifecycle digest entry and retain the published 0.15.0 pins unchanged.
7
+ - Cover package-named control.sh refusal logs and rejected `.executor` / `.policy.json` staging cleanup.
8
+ - Executor task definitions change: Apps rebuilt with SDK 0.15.1 require one administrator reapproval after upgrade.
9
+
10
+ 繁中摘要:e2e tunnel 必須明確設定 `DSM_E2E_SSH`,profile 預設 target 改為 `dsm`(沿用舊 profile 時設定 `DSM_E2E_TARGET`);清理內部 NAS 範例資訊。templates/SDK pins 同步 0.15.1;新增同版三架構 executor/lifecycle digest,保留 0.15.0 pins。驗證 control.sh 拒絕 log 的套件名與 runtime copies 清除。**task 定義改變:App 使用新 SDK 重建升級後,管理員需重新核准一次。**
11
+
3
12
  ## 0.15.0 (2026-10-18)
4
13
 
5
14
  - Add `synopkgland ctl` host/session management, authenticated API v1 discovery, explicit command trust, typed invocation and fixed job/pagination/NDJSON/upload flows. Commands keep App backend authorization; writes require confirmation.
@@ -329,7 +338,7 @@ before its timeout.
329
338
  - The window approval of an upgrade (outdated tasks) makes 11 scheduler calls
330
339
  instead of 12: the outdated boot task is deleted by the approval's own first
331
340
  EventScheduler get, made moments before the password, instead of a second
332
- get (2.9 s on vt-nas in t-0484). The control task is still read again by id
341
+ get (2.9 s on the test NAS in t-0484). The control task is still read again by id
333
342
  before its delete (spec/scheduler/v1). A fresh approval stays at 8.
334
343
  繁中:升級核准的排程呼叫由 12 降為 11(boot 任務不再於刪除前重讀)。
335
344
  - A scope approval reads the executor's selection back every second at most
package/README.md CHANGED
@@ -150,12 +150,17 @@ await dismissNotifications(); // when a card shows later
150
150
  ```
151
151
 
152
152
  `runWithTunnel` (the `tunnel --` command) opens an SSH tunnel to the NAS
153
- (`DSM_E2E_SSH`, default `root@vt-nas.syno`) on a free loopback port and runs the
153
+ (`DSM_E2E_SSH`, required, e.g. `admin@nas.example`) on a free loopback port and runs the
154
154
  command with `DSM_E2E_URL` set. It masks DSM's session cookies (`id`, `did`,
155
155
  `_SSID`), `X-SYNO-TOKEN`, `SynoToken` and `_sid` as `***` in the command's
156
156
  output and in the files under `DSM_E2E_OUTPUT_DIR` (default `.e2e`), and makes
157
157
  that directory private (700, files 600); saved sessions keep their cookies.
158
- Accounts come from mode 600 files named by `DSM_E2E_ACCOUNT_<USER>`.
158
+ Set `DSM_E2E_SSH` to your NAS SSH destination in the environment or a local
159
+ untracked `.env`; an unset or blank value fails before SSH or the command starts.
160
+ `nas.example` is a fixture hostname; replace it with your own host. Accounts come
161
+ from mode 600 files named by `DSM_E2E_ACCOUNT_<USER>`. Saved profiles use
162
+ `DSM_E2E_TARGET` (default `dsm`) and the user as their names; set the target to
163
+ an existing profile's target name to keep reusing it.
159
164
  `signIn` and `openDesktop` also register those values as e2e secrets in the
160
165
  test's worker, so e2e masks them in its own report. A test that forwards
161
166
  requests with `context.route` and `route.fetch` should end with
@@ -3,7 +3,7 @@ import { collectInstallParameters } from "./load.js";
3
3
  * Install parameter types that the schema still accepts but that have no working
4
4
  * install path on the live package-only route.
5
5
  *
6
- * Measured on vt-nas 2026-08-27: both end in DSM broken/150 with no Container
6
+ * Measured on the test NAS 2026-08-27: both end in DSM broken/150 with no Container
7
7
  * Manager project, while a no-parameter control package installed fine on the
8
8
  * same NAS. `secret` renders its password field and fails; `homes` renders no
9
9
  * input at all. `check` reports these as warnings so build keeps working
@@ -33,7 +33,7 @@ export function validateContract(parsed, contractFile) {
33
33
  }
34
34
  // DSM 7.4 Package Center refuses an SPK whose package identity exceeds 32
35
35
  // characters -- "Invalid file format", before the install starts and with
36
- // nothing written to synopkg.log. Measured on 10.17.83.200: 32 installs, 33
36
+ // nothing written to synopkg.log. Measured on a DSM 7.4 test NAS: 32 installs, 33
37
37
  // does not.
38
38
  if (typeof contract.package === "string" && contract.package.length > DSM_PACKAGE_IDENTITY_MAX) {
39
39
  const pos = getValuePosition(doc, ["package"]) ?? { line: 1, column: 1 };
@@ -164,7 +164,7 @@ function validateInstallParameters(parsed, contractFile) {
164
164
  }
165
165
  }
166
166
  // A45/A46: both types are in the schema enum but neither has a working
167
- // install path on the live package-only route -- measured on vt-nas as DSM
167
+ // install path on the live package-only route -- measured on the test NAS as DSM
168
168
  // broken/150 with no Container Manager project, against a no-parameter
169
169
  // control that installed fine. Refusing here is what makes the failure
170
170
  // honest: `homes` used to validate, build a valid SPK, and only break at
package/dist/e2e/dsm.d.ts CHANGED
@@ -17,7 +17,7 @@ type Page = ReturnType<NonNullable<ReturnType<typeof surfaceOf>>["page"]>;
17
17
  type Locator = ReturnType<Page["locator"]>;
18
18
  /** The DSM URL the browser opens: DSM_E2E_URL, else the local end of the SSH tunnel. */
19
19
  export declare function dsmUrl(env?: Env): string;
20
- /** ssh arguments forwarding the loopback ports of `forwards` to the NAS. */
20
+ /** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
21
21
  export declare function tunnelArgs(env?: Env): string[];
22
22
  /** A loopback port nothing listens on now, for a tunnel that names none. */
23
23
  export declare function freePort(): Promise<number>;
@@ -122,7 +122,7 @@ export declare function waitForDesktop(page?: Page): Promise<void>;
122
122
  /**
123
123
  * Closes every DSM notification card; returns how many were closed. DSM shows
124
124
  * them a moment after the desktop (the admin's "Admin account security" card
125
- * about 2 s later on vt-nas), so it first waits up to
125
+ * about 2 s later on a DSM 7.4 test NAS), so it first waits up to
126
126
  * DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
127
127
  */
128
128
  export declare function dismissNotifications(page?: Page, env?: Env): Promise<number>;
package/dist/e2e/dsm.js CHANGED
@@ -41,11 +41,14 @@ function forwards(env) {
41
41
  ...extra(env.DSM_E2E_TUNNEL_HTTPS_PORT, env.DSM_E2E_REMOTE_HTTPS_PORT, "443"),
42
42
  ];
43
43
  }
44
- /** ssh arguments forwarding the loopback ports of `forwards` to the NAS. */
44
+ /** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
45
45
  export function tunnelArgs(env = process.env) {
46
+ const target = env.DSM_E2E_SSH?.trim();
47
+ if (!target)
48
+ throw new Error("DSM_E2E_SSH is required to open a NAS tunnel; set it to user@nas.example (using your NAS host).");
46
49
  return ["-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes",
47
50
  ...forwards(env).flatMap(([local, remote]) => ["-L", `127.0.0.1:${local}:127.0.0.1:${remote}`]),
48
- env.DSM_E2E_SSH || "root@vt-nas.syno"];
51
+ target];
49
52
  }
50
53
  /** A loopback port nothing listens on now, for a tunnel that names none. */
51
54
  export function freePort() {
@@ -80,12 +83,13 @@ export async function runWithTunnel(command, outer = process.env) {
80
83
  throw new Error("runWithTunnel needs a command to run.");
81
84
  const port = Number(outer.DSM_E2E_TUNNEL_PORT || await freePort());
82
85
  const env = { ...outer, DSM_E2E_TUNNEL_PORT: String(port) };
86
+ const args = tunnelArgs(env);
83
87
  const ports = forwards(env).map(([local]) => Number(local));
84
88
  for (const used of ports) {
85
89
  if (await portOpen(used))
86
90
  throw new Error(`Port ${used} is already in use; name another DSM_E2E_TUNNEL_* port or close it before opening the tunnel.`);
87
91
  }
88
- const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh", tunnelArgs(env), { stdio: ["ignore", "ignore", "inherit"] });
92
+ const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh", args, { stdio: ["ignore", "ignore", "inherit"] });
89
93
  let sshExited = false;
90
94
  ssh.once("exit", () => { sshExited = true; });
91
95
  try {
@@ -93,7 +97,7 @@ export async function runWithTunnel(command, outer = process.env) {
93
97
  for (const waiting of ports) {
94
98
  while (!(await portOpen(waiting))) {
95
99
  if (sshExited || Date.now() > deadline)
96
- throw new Error(`The SSH tunnel to ${tunnelArgs(env).at(-1)} did not open on port ${waiting}.`);
100
+ throw new Error(`The SSH tunnel to ${args.at(-1)} did not open on port ${waiting}.`);
97
101
  await new Promise(accept => setTimeout(accept, 100));
98
102
  }
99
103
  }
@@ -364,7 +368,7 @@ const notificationCloseSelector = ".v-notification-container .v-notification-pan
364
368
  /**
365
369
  * Closes every DSM notification card; returns how many were closed. DSM shows
366
370
  * them a moment after the desktop (the admin's "Admin account security" card
367
- * about 2 s later on vt-nas), so it first waits up to
371
+ * about 2 s later on a DSM 7.4 test NAS), so it first waits up to
368
372
  * DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
369
373
  */
370
374
  export async function dismissNotifications(page = dsmPage(), env = process.env) {
@@ -454,7 +458,7 @@ async function restoredSessionLive(page) {
454
458
  */
455
459
  export async function signIn(app, user = "admin", env = process.env) {
456
460
  const url = dsmUrl(env);
457
- const file = profilePath(env.DSM_E2E_TARGET || "vt-nas", user, env);
461
+ const file = profilePath(env.DSM_E2E_TARGET || "dsm", user, env);
458
462
  const context = surface().context();
459
463
  const profile = loadProfile(file, url);
460
464
  if (profile)
@@ -8,7 +8,7 @@ import type { SharePermissionRow } from "./share-permission.js";
8
8
  * the install proceed, while an empty list would read as a real denial and
9
9
  * refuse the install on a transport error.
10
10
  *
11
- * The APIs used were confirmed present on DSM 7.4 (vt-nas):
11
+ * The APIs used were confirmed present on DSM 7.4 (the test NAS):
12
12
  * `SYNO.Core.Share.Permission`, `SYNO.Core.Group`, `SYNO.Core.Group.Member`.
13
13
  * `SYNO.Core.User.Group` also exists on the NAS but has no generated
14
14
  * synoagentcli operation, which is why membership is derived group-by-group.
@@ -10,7 +10,7 @@
10
10
  * extra_values directly, and share permission can change after install.
11
11
  *
12
12
  * The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
13
- * before changing the predicate: on vt-nas, `newadmin` has is_writable=false on
13
+ * before changing the predicate: on the test NAS, `newadmin` has is_writable=false on
14
14
  * its own row for `pkgland-p0b` and still writes there, because the
15
15
  * `administrators` group row carries the grant. A user-row-only check rejects
16
16
  * the only configuration with live proof behind it.
@@ -10,7 +10,7 @@
10
10
  * extra_values directly, and share permission can change after install.
11
11
  *
12
12
  * The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
13
- * before changing the predicate: on vt-nas, `newadmin` has is_writable=false on
13
+ * before changing the predicate: on the test NAS, `newadmin` has is_writable=false on
14
14
  * its own row for `pkgland-p0b` and still writes there, because the
15
15
  * `administrators` group row carries the grant. A user-row-only check rejects
16
16
  * the only configuration with live proof behind it.
@@ -9,5 +9,16 @@
9
9
  "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
10
10
  },
11
11
  "goToolchain": "go1.26.9"
12
+ },
13
+ "0.15.1": {
14
+ "x86_64": "4f47fb43f6e60dbad064afb0bbec96dd009fa046670e2c4b590b128a855cb404",
15
+ "aarch64": "c903b09ad657a838f0c62b6688e65e9094572b22b969770fca92fcc3640a5364",
16
+ "armv7": "8178d90de7ea720530bb9496e50ed118086b0e57382d84675aadc68f482d219f",
17
+ "lifecycle": {
18
+ "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
19
+ "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
20
+ "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
21
+ },
22
+ "goToolchain": "go1.26.9"
12
23
  }
13
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@napi-rs/keyring": "^2.1.1",
29
- "@synopackageland/templates": "0.15.0",
29
+ "@synopackageland/templates": "0.15.1",
30
30
  "ajv": "^8.17.1",
31
31
  "commander": "^13.1.0",
32
32
  "json-canonicalize": "^3.0.1",
package/skills/SKILL.md CHANGED
@@ -129,7 +129,7 @@ sweep forward the cancel instead of admitting it.
129
129
  `ExportExample` creates a private JSON file, calls `MoveFromStaging` once, and
130
130
  repeats the original request on repeated IDs. Review `example.go` to
131
131
  adapt a real producer; show Saved only from a protected committed receipt.
132
- - Wizard support is measured on vt-nas DSM 7.4 via a session-only SSH local
132
+ - Wizard support is measured on the test NAS DSM 7.4 via a session-only SSH local
133
133
  forward. This is not proof of other DSM releases or full writer/reboot behavior.
134
134
 
135
135
  ## 開啟方式
@@ -23,7 +23,7 @@ compose:
23
23
  network: {egress: isolated}
24
24
  # Private package binds are rejected by DSM build; keep compatible upstream named volumes.
25
25
  identity_gateway:
26
- sdk: 0.15.0
26
+ sdk: 0.15.1
27
27
  mode: none
28
28
  access: dsm-users
29
29
  listen: {http: 28000}
@@ -151,7 +151,7 @@ declared availability readiness, LAN isolation/egress, protected grant lifecycle
151
151
  ACLs, two-user visibility and clean install/upgrade/reboot/stop/uninstall before
152
152
  calling the package supported on a NAS. No NAS is accessed by the build gate.
153
153
 
154
- The unified Compose contract requires SDK, CLI and templates 0.15.0. Use the published, matching versions together; the legacy packaging route and bridge publish mode are removed.
154
+ The unified Compose contract requires SDK, CLI and templates 0.15.1. Use the published, matching versions together; the legacy packaging route and bridge publish mode are removed.
155
155
 
156
156
  Primary override files, and each included override set, must share a directory. Cross-directory override bases are rejected before loader I/O. Included implicit .env files are confined, inventoried and preserved; symlinks are rejected.
157
157
 
@@ -198,7 +198,7 @@ resources and does not migrate or delete retained data.
198
198
  Bridge web ingress uses a dedicated `sdk_ingress` internal bridge. The web service
199
199
  receives a fixed IPv4 address, and the package-account gateway connects directly
200
200
  to its declared container port. Internal bridges were reachable by package-account
201
- hooks in the sa3200d and vt-nas feasibility probes. No HAProxy helper, published
201
+ hooks in feasibility probes on an SA3200D and another test NAS. No HAProxy helper, published
202
202
  container port, Docker socket or host root is required. Explicit internet egress
203
203
  still adds the existing outbound bridge; other App services retain their internal networks.
204
204
 
@@ -231,7 +231,7 @@ Legacy `.sc` residue is not purged by this topology. Migrating an already publis
231
231
  installation requires a separately authorized cleanup or an official worker fix;
232
232
  a no-publish rebuild cannot claim to remove historical service registrations.
233
233
 
234
- On vt-nas DSM 7.4/Docker 24.0.2, internal networks blocked tested external IPv4
234
+ On the test NAS DSM 7.4/Docker 24.0.2, internal networks blocked tested external IPv4
235
235
  and external DNS, but allowed NAS host services through the bridge gateway and
236
236
  NAS LAN address. Therefore `isolated` is not a host-service firewall. IPv6 had
237
237
  no usable default route on this fixture, which is not proof of an IPv6 firewall.
@@ -245,7 +245,7 @@ Set-Cookie. Other App sessions/native cookies retain their normal semantics.
245
245
  Cookies are shared across ports on the same hostname, so filtering only this
246
246
  package's SDK cookie would leak another App's SDK session to an upstream.
247
247
 
248
- Historical publish-topology vt-nas validation was release-blocked: the Container Manager
248
+ Historical publish-topology validation on the test NAS was release-blocked: the Container Manager
249
249
  24.0.2-1706 docker-project worker can return profile error 1202 on update,
250
250
  which Package Center reports as postinst-worker failure 277. The owned test
251
251
  project and a real Vaultwarden package reproduced this; switching Compose
@@ -328,7 +328,7 @@ The [docker-project contract](https://help.synology.com/developer-guide/resource
328
328
  documents project deletion on PREUNINST and build flags, without a separate
329
329
  package-account profile-release option.
330
330
 
331
- The original isolated Header fixture on vt-nas installed and started from a
331
+ The original isolated Header fixture on the test NAS installed and started from a
332
332
  clean configuration, then its upgrade still returned port conflict → 1202 →
333
333
  277. Historical `.sc` residue alone is therefore insufficient to explain or
334
334
  resolve that environment's upgrade failure. Preserve the timestamp/PID chain;
@@ -40,8 +40,8 @@ rootOps 與 delegate 已共用管理員 PasswordConfirm 核准的 protected exec
40
40
  backend 寫 `runtime: python|node|rust`(`runtime: go` 等同 `kind: go`)。CGI 是 SDK 的 Go runtime(`bridge/cmd/runtime`),它驗證 DSM session、entry 與 `permissions.json` 後,以套件帳號(不是 root)每個請求啟動一次 App。App 放在 ui 以外的目錄,打包成 `target/app/`,build 寫入 `bridge.json`(spec/bridge/v1)並把它與 `permissions.json` 的 sha256 編進 runtime。
41
41
 
42
42
  ```yaml
43
- backend: {runtime: python, dir: app, entry: main.py, sdk: 0.15.0}
44
- backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.15.0, node: {package: Node.js_v20}}
43
+ backend: {runtime: python, dir: app, entry: main.py, sdk: 0.15.1}
44
+ backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.15.1, node: {package: Node.js_v20}}
45
45
  backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet]}
46
46
  ```
47
47
 
@@ -213,7 +213,7 @@ sweep forward the cancel instead of admitting it.
213
213
  `ExportExample` creates a private JSON file, calls `MoveFromStaging` once, and
214
214
  repeats the original request on repeated IDs. Review `example.go` to
215
215
  adapt a real producer; show Saved only from a protected committed receipt.
216
- - Wizard support is measured on vt-nas DSM 7.4 via a session-only SSH local
216
+ - Wizard support is measured on the test NAS DSM 7.4 via a session-only SSH local
217
217
  forward. This is not proof of other DSM releases or full writer/reboot behavior.
218
218
 
219
219
  ## 開啟方式
@@ -118,7 +118,7 @@ sweep forward the cancel instead of admitting it.
118
118
  `ExportExample` creates a private JSON file, calls `MoveFromStaging` once, and
119
119
  repeats the original request on repeated IDs. Review `example.go` to
120
120
  adapt a real producer; show Saved only from a protected committed receipt.
121
- - Wizard support is measured on vt-nas DSM 7.4 via a session-only SSH local
121
+ - Wizard support is measured on the test NAS DSM 7.4 via a session-only SSH local
122
122
  forward. This is not proof of other DSM releases or full writer/reboot behavior.
123
123
 
124
124
  The pinned scope-selection task also binds the protected generation observed before confirmation. The executor compares it under the publish lock and refuses stale tasks, so a retained task cannot replay an older selection after another confirmation.