@synopackageland/cli 0.41.1 → 0.41.3

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
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.41.3 (2026-10-30)
6
+
7
+ - Build Portal Apps without an executor on DSM 7.2-64570+ using the App SDK
8
+ pin for the panel text. Keep executor SDK selection unchanged.
9
+ - Reject a no-executor Portal without an SDK during both check and build
10
+ (`STANDALONE_PORTAL_SDK_REQUIRED`); cover the Portal/executor/SDK/DSM matrix
11
+ and a real Go CGI-to-SPK build.
12
+ - Complete diagnostics for invalid external caller declarations and entry policies.
13
+
14
+ ## 0.41.2 (2026-10-29)
15
+
16
+ - Require templates ~0.35.2 with SDK/App 0.38.1 and clean registry metadata.
17
+
5
18
  ## 0.41.1 (2026-10-29)
6
19
 
7
20
  - `ui.banner: shown | hidden` lets an App choose whether DSM draws its banner
package/README.md CHANGED
@@ -76,13 +76,19 @@ and checks both payloads against the resolved SDK's `executor-manifest.json`.
76
76
  The manifest declares format 1, the exact executor release, its Go toolchain and
77
77
  six SHA-256 digests. SDK minor versions are independent of CLI minor versions.
78
78
 
79
+ Portal panel text comes from the App's SDK even when it has no executor: the
80
+ existing `identity_gateway.sdk` pin, or otherwise the backend SDK requirement
81
+ (`backend/go.mod`, `backend.sdk`, Rust SDK tag, or project `go.mod` for a binary).
82
+ A Portal without an SDK is rejected by both `check` and `build` with
83
+ `STANDALONE_PORTAL_SDK_REQUIRED`. DSM 7.2-64570+ needs no executor for Portal.
84
+
79
85
  SDK module downloads preserve normal Go checksum verification and the user's Go
80
86
  settings; Go Apps also check their existing `backend/go.sum`. Commit that file
81
87
  alongside npm lockfiles. Do not disable checksum verification for public SDK
82
88
  packages. Private mirrors can use explicitly configured Go authentication and
83
89
  checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
84
90
 
85
- CLI 0.41.1 uses templates ~0.35.1, whose default SDK pin is 0.38.0. Compose import and SDK defaults
91
+ CLI 0.41.3 uses templates ~0.35.2, whose default SDK pin is 0.38.1. Compose import and SDK defaults
86
92
  read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
87
93
  Every App method must be declared in the permissions table. Dynamic names and
88
94
  custom HTTP dispatchers are rejected. With SDK 0.33.0, use fixed
@@ -93,7 +99,7 @@ unimported development commands are excluded. Use
93
99
  authentication. See [App entry and registration](docs/app-entry.md).
94
100
 
95
101
  `init` and `add` read their templates from the locally installed
96
- `@synopackageland/templates`. CLI 0.41.1 depends on `~0.35.1`: npm installation or
102
+ `@synopackageland/templates`. CLI 0.41.3 depends on `~0.35.2`: npm installation or
97
103
  update can select the latest templates patch in the 0.35 minor line. Templates
98
104
  patches need only a templates release; update the npm installation to receive them.
99
105
  The CLI never fetches templates at runtime, so offline use works after installation;
@@ -38,6 +38,21 @@ export const ERROR_REGISTRY = {
38
38
  description: "The launcher has Portal fields without a matching explicit Portal declaration.",
39
39
  suggestion: "Remove defaultAlias and login fields, or explicitly enable portal and rebuild.",
40
40
  },
41
+ CTL_EXTERNAL_TRANSPORT: {
42
+ code: "CTL_EXTERNAL_TRANSPORT",
43
+ description: "External callers require the DSM session transport.",
44
+ suggestion: "Use the DSM session transport for external entries; remove capabilities.identity from an external-enabled App.",
45
+ },
46
+ CTL_EXTERNAL_ENTRY: {
47
+ code: "CTL_EXTERNAL_ENTRY",
48
+ description: "An external entry is not a registered administrator RPC method.",
49
+ suggestion: "Register a fixed RPC method, declare it as administrators in permissions.json, and reference it in ctl.external.",
50
+ },
51
+ CTL_EXTERNAL_DECLARATION_INVALID: {
52
+ code: "CTL_EXTERNAL_DECLARATION_INVALID",
53
+ description: "ctl.external contains an invalid protocol or RPC method declaration.",
54
+ suggestion: "Use protocol names mapped to objects with a fixed method, for example ctl.external.mcp: {method: app.mcp}.",
55
+ },
41
56
  LAUNCHER_TYPE_MISMATCH: {
42
57
  code: "LAUNCHER_TYPE_MISMATCH",
43
58
  description: "The built launcher or INFO entry differs from the declared launch mode.",
@@ -618,6 +633,11 @@ export const ERROR_REGISTRY = {
618
633
  description: "console is declared together with portal.",
619
634
  suggestion: "Declare either console or portal, not both.",
620
635
  },
636
+ STANDALONE_PORTAL_SDK_REQUIRED: {
637
+ code: "STANDALONE_PORTAL_SDK_REQUIRED",
638
+ description: "A Portal App has no SDK pin for its panel text.",
639
+ suggestion: "Declare the App SDK in backend/go.mod (project go.mod for a binary), backend.sdk, the Rust SDK dependency, or identity_gateway.sdk; otherwise remove portal. An executor is not required on DSM 7.2-64570+.",
640
+ },
621
641
  STANDALONE_PORTAL_REQUIRES_EXECUTOR: {
622
642
  code: "STANDALONE_PORTAL_REQUIRES_EXECUTOR",
623
643
  description: "An App targeting DSM below 7.2-64570 declares portal without an executor.",
@@ -25,6 +25,7 @@ import { isBridgeBackend } from "./bridge-backend.js";
25
25
  import { requestTimeoutSeconds } from "./request-timeout.js";
26
26
  import { buildBridge, prepareBridge, } from "./bridge-build.js";
27
27
  import { portalCatalogs } from "./portal-window.js";
28
+ import { portalSdkVersion } from "./portal.js";
28
29
  /** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
29
30
  export const runCommand = (command, args, options) => new Promise((done) => {
30
31
  const child = spawn(command, args, {
@@ -159,9 +160,9 @@ export async function buildStandalone(options) {
159
160
  const installSettingsControl = await buildInstallSettings(manifest, arch, archWork, run);
160
161
  const appBackupControl = await buildAppBackup(manifest, arch, archWork, run, (w) => warnings.add(w));
161
162
  const composeStorageInit = await buildComposeStorageInit(manifest, arch, archWork, run);
162
- // A portal needs the executor, so the executor's SDK carries the panel text.
163
- const portalText = manifest.portal && executor
164
- ? portalCatalogs(sdkSource(executor.sdk).goDir)
163
+ // Executor Apps keep their executor's SDK; no-root Apps use their own SDK pin.
164
+ const portalText = manifest.portal
165
+ ? portalCatalogs(sdkSource(executor?.sdk ?? portalSdkVersion(manifest)).goDir)
165
166
  : undefined;
166
167
  await assembleStandaloneSpk({
167
168
  manifest: builtManifest,
@@ -30,6 +30,7 @@ import { LOGIN_COLOR, loginImageProblem, } from "../build-spk/login-style.js";
30
30
  import { validateReproducible } from "../reproducible-config.js";
31
31
  import { appSdkVersion, SDK_MODULE, sdkSource, } from "../runtime/sdk-source.js";
32
32
  import { sdkProblems } from "./sdk-check.js";
33
+ import { portalSdkVersion } from "./portal.js";
33
34
  import { readWindowSize } from "./window-size.js";
34
35
  import { appSdkTimeouts } from "../runtime/sdk-timeouts.js";
35
36
  import { requestTimeoutSeconds, rootRunDeadlineProblem, TIMEOUT_USAGE, validTimeoutSeconds, } from "./request-timeout.js";
@@ -466,6 +467,16 @@ function readManifest(raw, c) {
466
467
  if (portal && osMinVer && !isDsmAtLeast(osMinVer, PORTAL_EXECUTOR_MIN_DSM)) {
467
468
  c.fail("STANDALONE_PORTAL_MIN_DSM", portalMinDsmMessage(osMinVer, "portal"));
468
469
  }
470
+ if (portal && backend && osMinVer && !portalByExecutor(osMinVer) &&
471
+ !capabilities.rootOps && !capabilities.delegate && !capabilities.webapi) {
472
+ try {
473
+ if (!portalSdkVersion({ backend, root: c.root, identityGateway }))
474
+ c.fail("STANDALONE_PORTAL_SDK_REQUIRED", "portal needs the App's SDK for its panel text: require github.com/synopackageland/syno-pkg-land-sdk in the Go backend's go.mod (project go.mod for a binary), set backend.sdk for Python/Node, pin the Rust SDK release, or use identity_gateway.sdk. Remove portal if the App has no SDK.");
475
+ }
476
+ catch {
477
+ // sdkProblems reports an invalid backend SDK pin above.
478
+ }
479
+ }
469
480
  const entryProblem = portal && packageId && displayname && osMinVer && portalByExecutor(osMinVer)
470
481
  ? portalEntryProblem(packageId, {
471
482
  application: displayname,
@@ -1,5 +1,7 @@
1
1
  import type { StandaloneManifest } from "./manifest.js";
2
2
  import type { PortalPackage } from "../portal/package.js";
3
+ /** The App's existing gateway pin or backend SDK; never the CLI/templates default. */
4
+ export declare function portalSdkVersion(manifest: Pick<StandaloneManifest, "backend" | "root" | "identityGateway">): string | undefined;
3
5
  /**
4
6
  * A native portal registered through DSM's web-config resource; it opens the
5
7
  * package's own DSM window. Below DSM 7.2 the executor registers it instead.
@@ -1,4 +1,9 @@
1
1
  import { portalByExecutor } from "../build-spk/dsm-version.js";
2
+ import { appSdkVersion } from "../runtime/sdk-source.js";
3
+ /** The App's existing gateway pin or backend SDK; never the CLI/templates default. */
4
+ export function portalSdkVersion(manifest) {
5
+ return manifest.identityGateway?.sdk ?? appSdkVersion(manifest.backend.kind === "binary" ? { kind: "go", dir: manifest.root } : manifest.backend);
6
+ }
2
7
  /**
3
8
  * A native portal registered through DSM's web-config resource; it opens the
4
9
  * package's own DSM window. Below DSM 7.2 the executor registers it instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.41.1",
3
+ "version": "0.41.3",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@napi-rs/keyring": "^2.1.1",
31
- "@synopackageland/templates": "~0.35.1",
31
+ "@synopackageland/templates": "~0.35.2",
32
32
  "ajv": "^8.17.1",
33
33
  "commander": "^13.1.0",
34
34
  "json-canonicalize": "^3.0.1",
package/skills/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
5
5
 
6
6
  # Independent Synology SPKs
7
7
 
8
- The CLI installs templates through `~0.35.1`. A templates patch needs only a
8
+ The CLI installs templates through `~0.35.2`. A templates patch needs only a
9
9
  templates release; npm install/update selects a patch in that minor line. The CLI
10
10
  uses the locally installed templates and reads their SDK pin for defaults; it
11
11
  never downloads templates at runtime. Preserve the lockfile and use `npm ci` for
@@ -24,7 +24,7 @@ Use this guide to initialize, build and inspect own applications or upstream OSS
24
24
 
25
25
  For an existing open-source App, first read [reference/upstream-fork.md](reference/upstream-fork.md): upstream fork/history + untouched upstream files + all packaging in `synology/`, official container images pinned by tag/index digest, native official release binaries pinned by SHA-256 or a source build hook, and a single tag-merge upgrade command. Use `init --upstream-fork`; never rewrite the App as a component. Integrated upstream Apps require direct DSM administrator entry without a separate App password or an OIDC/SSO server: supported trusted headers first, disabling native login second; the shipped OpenList pattern uses the existing SDK 0.17.3 token adapter (`mode: token`, `access: dsm-admins`, `entry: admin-only`) with server-side Authorization injection. All DSM admins share one upstream admin; token reset requires package restart; WebDAV retains its native credentials.
26
26
 
27
- Use SDK/App 0.38.0 and CLI 0.41.1 for App entry enforcement. Declare every App method
27
+ Use SDK/App 0.38.1 and CLI 0.41.3 for App entry enforcement. Declare every App method
28
28
  in the permissions table; dynamic names and unregistered HTTP dispatchers are rejected.
29
29
  Use fixed SDK routes and `identity_gateway.upstream_auth_paths` for client API
30
30
  paths that rely on upstream authentication. Read
@@ -41,7 +41,7 @@ cannot upgrade a connection; use an existing upgrade-capable deployment
41
41
  transport. The audit follows each configured Go binary's Linux import graph,
42
42
  including declared workers; unshipped dev commands are excluded.
43
43
 
44
- SDK/App 0.38.0 allows 300 seconds for requests by default, with the browser
44
+ SDK/App 0.38.1 allows 300 seconds for requests by default, with the browser
45
45
  waiting 302 seconds. Explicit budgets and earlier parent deadlines still win.
46
46
  For long work, use existing independent workers with prompt job admission,
47
47
  progress and authorized status/result polling by the original ID. A timed-out
@@ -62,7 +62,7 @@ backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet]}
62
62
 
63
63
  ## 請求期限與視窗尺寸
64
64
 
65
- SDK/App 0.38.0 的 backend 請求期限預設 300 秒,DSM 視窗與 TS client 預設等候 302 秒。CLI 從 App 所選 SDK 的 timeout spec 讀取預設;`backend.timeout_seconds`(1–3600 的整數)可明確覆寫,go、python、node、rust 都適用,較早的 parent deadline 仍優先。到期時 App 回 `request_timeout`;DSM 視窗把工作期限放進 `ctx.requestTimeout`(毫秒),`ctx.call` 與 SDK 瀏覽器 client 多等 2 秒,讓 backend 的回覆先到。已安裝的 App、舊 SDK 或既有明確 timeout 不會自動改變;需使用匹配的 Go/TS SDK 重建。
65
+ SDK/App 0.38.1 的 backend 請求期限預設 300 秒,DSM 視窗與 TS client 預設等候 302 秒。CLI 從 App 所選 SDK 的 timeout spec 讀取預設;`backend.timeout_seconds`(1–3600 的整數)可明確覆寫,go、python、node、rust 都適用,較早的 parent deadline 仍優先。到期時 App 回 `request_timeout`;DSM 視窗把工作期限放進 `ctx.requestTimeout`(毫秒),`ctx.call` 與 SDK 瀏覽器 client 多等 2 秒,讓 backend 的回覆先到。已安裝的 App、舊 SDK 或既有明確 timeout 不會自動改變;需使用匹配的 Go/TS SDK 重建。
66
66
 
67
67
  - python、node、rust:build 把它寫進 `bridge.json` 的 `timeout_seconds`;runtime 以它為期限,傳給 App 的 `deadline_ms` 是送出呼叫時剩下的時間。
68
68
  - go:build 以 `-X main.requestTimeout=<秒>` 傳入。要寫 `timeout_seconds`,`main.go` 必須讀這個變數,否則 check 回 `STANDALONE_TIMEOUT_UNSUPPORTED`;未設定時使用所選 SDK 的 `api.DefaultRequestTimeout`:
@@ -93,7 +93,7 @@ func deadline() api.DeadlineOption {
93
93
 
94
94
  直接 File Station 瀏覽器讀取預設也為 302 秒;identity gateway 等候 upstream headers 預設 310 秒,自訂期限需對齊 gateway 與外部 proxy。這些預設不保證所有 DSM/proxy 都能完成五分鐘請求。executor batch 的 60 秒上限、已核准 root policy/program budget 與 credential lifetime 維持原契約;長 root 工作使用既有 `root.start` / `root.jobs.*`。
95
95
 
96
- 獨立 `ctl` transport 預設仍是 62 秒。長同步呼叫須明確指定 `--timeout`,或使用既有 nonblocking job flow;參考 [ctl](ctl.md)。完整契約見 SDK 的 [request deadlines](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.38.0/docs/request-deadlines.md)。
96
+ 獨立 `ctl` transport 預設仍是 62 秒。長同步呼叫須明確指定 `--timeout`,或使用既有 nonblocking job flow;參考 [ctl](ctl.md)。完整契約見 SDK 的 [request deadlines](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.38.1/docs/request-deadlines.md)。
97
97
 
98
98
  DSM 視窗預設 960×640、最小 700×420;`ui.window` 只寫要改的欄位:
99
99
 
@@ -132,7 +132,7 @@ every App supports cancellation or singleton deduplication.
132
132
 
133
133
  For AI consumers:
134
134
 
135
- The independent ctl transport defaults to 62 seconds even with SDK/App 0.38.0.
135
+ The independent ctl transport defaults to 62 seconds even with SDK/App 0.38.1.
136
136
  For a long synchronous call, set `--timeout` explicitly; for an existing job
137
137
  flow, use `--no-wait` and the original ID. A status read timeout leaves the last
138
138
  known queued/running state intact. If the App's admission API supports an
@@ -2,6 +2,8 @@
2
2
 
3
3
  Tier: a schema-2 App targeting DSM 7.2-64570+ may declare a Portal without an executor, including a unified Compose App. The CLI adds `instuninst_restart_services="nginx.service"` to INFO (merging service names without duplicates), so DSM reloads nginx after install, upgrade and uninstall. App hooks still run as the package account; a no-privilege App never runs root code. Declaring a Portal does not activate an alias or automatically open it: an administrator must apply the alias through DSM.
4
4
 
5
+ Portal needs the App's SDK for its panel text, including the no-executor route. The CLI uses the existing `identity_gateway.sdk` pin when present; otherwise it reads the backend SDK (`backend/go.mod` for Go, `backend.sdk` for Python/Node, the Rust release tag in Cargo.lock, or project `go.mod` for a binary). It does not substitute the CLI/templates SDK default. Without an SDK, `check` and `build` both refuse `STANDALONE_PORTAL_SDK_REQUIRED`: declare the App's SDK or remove Portal. Executor Apps continue using their executor's SDK.
6
+
5
7
  On DSM 7.1 the unsigned web-config worker is unavailable, so entry registration still requires an executor (`rootOps`, `delegate` or `webapi`). `check`/`build` and `add portal` refuse a no-executor App targeting below 7.2 with `STANDALONE_PORTAL_REQUIRES_EXECUTOR`; raise `os_min_ver` to 7.2-64570+, add an executor capability, or omit Portal. `add portal` preserves a supported DSM target; it does not silently upgrade a no-privilege App's target or add root capabilities. The legacy `integration.web.portal` contract is unsupported; use the schema-2 top-level `portal` declaration.
6
8
 
7
9
  Declare: `portal: {}` or `portal: {alias: my-app}` in dsm-package.yaml (`synopkgland add portal` with os_min_ver 7.2-64570+, or `synopkgland add root-ops portal` for the DSM 7.1 executor route). The alias defaults to the package name in lowercase. Aliases are 1–64 lowercase letters, digits or `-`, not starting or ending with `-`, and not a DSM path or Synology App alias (`webman`, `file`, …). The native-layout `backend: {kind: native}` owns its conf/scripts and its own root helper: `portal: {}` or `{alias}` there needs os_min_ver 7.1-42661+ and writes `synopkgland_portal_alias="<alias>"` and the nginx reload declaration into INFO, which Package Center writes as root, so the product's root helper can trust it; the helper registers the app.d entry (application = INFO displayname) and owns its uninstall cleanup and wizard. inspect checks that INFO line against `native-layout.json`.