@synopackageland/cli 0.15.4 → 0.17.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +22 -8
  2. package/README.md +8 -1
  3. package/assets/init-compose.md +33 -0
  4. package/assets/init-upstream/build-source.sh +10 -0
  5. package/assets/init-upstream/update-image.mjs +26 -0
  6. package/assets/init-upstream/upgrade.sh +47 -0
  7. package/assets/init-upstream/version.mjs +12 -0
  8. package/dist/add.js +3 -3
  9. package/dist/build-spk/uninstall-wizard.d.ts +1 -1
  10. package/dist/build-spk/uninstall-wizard.js +1 -1
  11. package/dist/build-spk/wizard-messages.js +9 -9
  12. package/dist/cli.js +16 -2
  13. package/dist/contract/validate.js +2 -2
  14. package/dist/e2e/dsm.d.ts +2 -2
  15. package/dist/e2e/dsm.js +3 -3
  16. package/dist/error-registry.js +8 -4
  17. package/dist/host/share-permission.d.ts +1 -1
  18. package/dist/host/share-permission.js +1 -1
  19. package/dist/init-upstream.d.ts +7 -0
  20. package/dist/init-upstream.js +64 -0
  21. package/dist/init.d.ts +4 -0
  22. package/dist/init.js +37 -1
  23. package/dist/portal/alias.d.ts +2 -6
  24. package/dist/portal/alias.js +2 -6
  25. package/dist/portal/info.d.ts +4 -0
  26. package/dist/portal/info.js +32 -0
  27. package/dist/runtime/dsm-version.d.ts +1 -1
  28. package/dist/runtime/dsm-version.js +1 -1
  29. package/dist/runtime/executor-digests.json +4 -26
  30. package/dist/runtime/sdk-batch.d.ts +2 -0
  31. package/dist/runtime/sdk-batch.js +23 -0
  32. package/dist/runtime/uninstall-cleanup.js +1 -1
  33. package/dist/skill.js +1 -0
  34. package/dist/standalone/build.js +8 -1
  35. package/dist/standalone/compose-errors.js +15 -0
  36. package/dist/standalone/compose.d.ts +8 -0
  37. package/dist/standalone/compose.js +13 -3
  38. package/dist/standalone/dsm-window.js +25 -4
  39. package/dist/standalone/identity-gateway.js +1 -0
  40. package/dist/standalone/inspect-spk.js +17 -3
  41. package/dist/standalone/lifecycle.js +4 -1
  42. package/dist/standalone/manifest.js +7 -2
  43. package/dist/standalone/native-layout.js +6 -2
  44. package/dist/standalone/portal-window.js +1 -1
  45. package/dist/standalone/spk.d.ts +3 -0
  46. package/dist/standalone/spk.js +9 -2
  47. package/package.json +3 -3
  48. package/skills/SKILL.md +8 -14
  49. package/skills/reference/compose.md +25 -8
  50. package/skills/reference/create.md +4 -4
  51. package/skills/reference/portal.md +10 -6
  52. package/skills/reference/ui-vue.md +12 -0
  53. package/skills/reference/upstream-fork.md +165 -0
  54. package/skills/reference/wrap.md +3 -1
@@ -16,6 +16,7 @@ export interface AssembleOptions {
16
16
  executor?: ExecutorArtifact;
17
17
  daemonControl?: string;
18
18
  composeControl?: string;
19
+ composeStorageInit?: string;
19
20
  /** The SDK identity gateway, required exactly when the manifest declares identity_gateway. */
20
21
  identityGateway?: string;
21
22
  /** The compiled background worker, required exactly when capabilities.jobs is enabled. */
@@ -33,3 +34,5 @@ export interface AssembleOptions {
33
34
  * authorization remains separate and requires explicit administrator approval.
34
35
  */
35
36
  export declare function assembleStandaloneSpk(options: AssembleOptions): Promise<void>;
37
+ /** Manifest validation already refused quotes, backslashes and newlines. */
38
+ export declare function renderInfo(manifest: StandaloneManifest, arch: NativeArchFamily, checksum: string, executorDigest?: string): string;
@@ -1,3 +1,4 @@
1
+ import { portalInfoFields } from "../portal/info.js";
1
2
  import { sdkSource } from "../runtime/sdk-source.js";
2
3
  import { resolvedSdkLaunch } from "../contract/app-launch.js";
3
4
  import { metadataInfoFields } from "../package-metadata.js";
@@ -190,6 +191,12 @@ export async function assembleStandaloneSpk(options) {
190
191
  throw new StandaloneBuildError("SDK_COMPOSE_BUILD_FAILED", "Compose runtime artifact required.");
191
192
  copyFileSync(options.composeControl, join(target, "control"));
192
193
  chmodSync(join(target, "control"), EXEC_MODE);
194
+ if (manifest.compose.plan.services.some((s) => s.storage_init)) {
195
+ if (!options.composeStorageInit)
196
+ throw new StandaloneBuildError("SDK_COMPOSE_BUILD_FAILED", "Compose storage initializer required.");
197
+ copyFileSync(options.composeStorageInit, join(target, "storage-init"));
198
+ chmodSync(join(target, "storage-init"), EXEC_MODE);
199
+ }
193
200
  }
194
201
  const packageTgz = join(staging, "package.tgz");
195
202
  await createReproducibleTar(join(staging, "package"), packageTgz, {
@@ -293,7 +300,7 @@ function quote(value) {
293
300
  return `"${value.replace(/([\\"$`])/g, "\\$1")}"`;
294
301
  }
295
302
  /** Manifest validation already refused quotes, backslashes and newlines. */
296
- function renderInfo(manifest, arch, checksum, executorDigest) {
303
+ export function renderInfo(manifest, arch, checksum, executorDigest) {
297
304
  const fields = [
298
305
  ["package", manifest.package],
299
306
  ["version", manifest.spkVersion],
@@ -334,7 +341,7 @@ function renderInfo(manifest, arch, checksum, executorDigest) {
334
341
  if (text?.description)
335
342
  fields.push([`description_${DSM_LANGS[locale]}`, text.description]);
336
343
  }
337
- return fields.map(([key, value]) => `${key}=${quote(value)}\n`).join("");
344
+ return portalInfoFields(fields, Boolean(manifest.portal)).map(([key, value]) => `${key}=${quote(value)}\n`).join("");
338
345
  }
339
346
  function copyUiTree(source, target) {
340
347
  for (const entry of ["app.js", "app.css"]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.15.4",
3
+ "version": "0.17.0",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "build": "rm -rf dist && tsc",
21
- "test": "npm run build && node --import tsx --test test/*.test.ts test/ctl/*.test.ts",
21
+ "test": "npm run build && node --import tsx --test --test-concurrency=${SYNOPKGLAND_GATE_JOBS:-3} test/*.test.ts test/ctl/*.test.ts",
22
22
  "test:glossary": "node --test scripts/test/glossary-drift.test.mjs",
23
23
  "test:scripts": "node --test scripts/test/gate.test.mjs scripts/test/release.test.mjs scripts/test/pack-docker.test.mjs scripts/test/npm-pack.test.mjs scripts/test/executor-bump.test.mjs scripts/test/smoke-exports.test.mjs scripts/test/executor-digests.test.mjs scripts/test/parallel.test.mjs scripts/test/scaffold-matrix.test.mjs scripts/test/frontend-deps.test.mjs scripts/test/docker-gate.test.mjs scripts/test/gate-plan.test.mjs scripts/test/gates.test.mjs",
24
24
  "gates": "bash scripts/ci/gates.sh",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@napi-rs/keyring": "^2.1.1",
30
- "@synopackageland/templates": "0.15.2",
30
+ "@synopackageland/templates": "0.17.1",
31
31
  "ajv": "^8.17.1",
32
32
  "commander": "^13.1.0",
33
33
  "json-canonicalize": "^3.0.1",
package/skills/SKILL.md CHANGED
@@ -7,13 +7,15 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
7
7
 
8
8
  Use this guide to initialize, build and inspect own applications or upstream OSS as independent packages. 依已實作能力區分 DSM session 檔案、私有 jobs 與 App 登入;delegate 已實作並隨 CLI 提供固定操作的離線委託(預設 `files.moveFromStaging`);共用 delegate/rootOps executor 的完整裝置生命週期驗收仍待完成,不得宣稱已通過;支援邊界依 delegate guide/support matrix,禁止發明新的身分平台。
9
9
 
10
+ 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.0 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.
11
+
10
12
  Start with `synopkgland init -t native-go`, `synopkgland init -t binary`, or `synopkgland init -t compose`. Read reference/create.md or reference/wrap.md, then reference/deploy.md. Native CGI session checks belong to the package. For wrapping upstream OSS with DSM accounts, read [reference/identity-gateway.md](reference/identity-gateway.md) first. For all Compose work, read [reference/compose.md](reference/compose.md): schema-2 upstream files, generated override, SDK planner, fixed package identity and current DSM blockers. Containers do not inherit a signed-in user's File Station ACL. Existing Shared Folder mounts are blocked until official protected ACL/project control is verified.
11
13
 
12
14
  Never install, uninstall, publish or modify NAS data without task authorization. Prefer check, inspect and deploy --dry-run for reviewable artifacts.
13
15
 
14
16
  Native projects use schema 2 capabilities and `--with files,jobs,login,root-ops,delegate`; use `add` for existing projects. Prefer `check --json`, `build --json`, `inspect <spk> --json`, `deploy --dry-run --json`, and `status --host <profile> --json` for structured results. Read the conflict policy in reference/create.md before adding overlays.
15
17
 
16
- Read reference/portal.md before declaring or changing a DSM Application Portal alias (`add portal`); DSM owns the saved setting, and only an App with rootOps or delegate may declare one. Read reference/files.md and reference/identity.md for capability boundaries. RootOps and delegate share one-time protected executor boot/control setup with digest approval. Existing native products can explicitly use the separate native runtimes in @synopackageland/sdk/native; do not mix its tasks with generated executor capabilities. App sessions alone do not grant NAS file authority.
18
+ Read reference/portal.md before declaring or changing a DSM Application Portal alias (`add portal`); DSM owns the saved setting; DSM 7.2-64570+ supports schema-2 Portal without an executor, while DSM 7.1 requires executor registration. Read reference/files.md and reference/identity.md for capability boundaries. RootOps and delegate share one-time protected executor boot/control setup with digest approval. Existing native products can explicitly use the separate native runtimes in @synopackageland/sdk/native; do not mix its tasks with generated executor capabilities. App sessions alone do not grant NAS file authority.
17
19
 
18
20
  Read [reference/lifecycle.md](reference/lifecycle.md) before editing `dsm-scripts/` (preinst, postinst, preupgrade, postupgrade, preuninst, postuninst, start-stop-status): the SDK runs them inside its generated scripts as the package account; never add root work or repeat SDK steps there.
19
21
 
@@ -149,18 +151,10 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
149
151
  `defaultAlias`、Portal 面板/capability 或 Web Station alias;沒有 Portal
150
152
  不是 inspect/info/doctor 問題。明確啟用 `portal` 時才將其 alias 作為 `defaultAlias`。
151
153
 
152
- Docker/web service App 設定 `integration.web.launch`,內建預設是 `new-page`;
153
- 省略開啟方式時直接開新分頁,不宣告 Application Portal。SDK 原生 App
154
- 省略時使用 `dsm-window`;非 UI SDK 原生 App 必須明確寫 `ui.launch: new-page`。
155
- Web service 主選單使用
156
- `type: "url"` 與明確的 `url` 指向既有 DSM gateway。明確 `access: public` 才保留 Web Station
157
- alias;這是服務入口,不是 SDK Application Portal。DSM 會將 standalone URL 改寫成 launchApp 頁面,
158
- 需要 AppInstance/AppWindow JS class;schema 1 web service 沒有該 class,因此使用 url。需要 DSM 視窗時明寫 `dsm-window`。
159
-
160
- Schema 2 的 `dsm-package.yaml` + `identity_gateway` + `compose` 同樣視為
161
- web service:省略 `ui.launch` 時預設 `new-page`,使用 standalone launcher,
162
- 不帶 Application Portal。兩條 Compose 路線共用 `contract/app-launch.ts`
163
- 解析開啟方式與選擇 launcher 型別;明確的 `ui.launch` 可覆寫預設。
164
- 只有沒有 web service 的 SDK 原生 App 省略時才預設 `dsm-window`。
154
+ Schema 2 Compose 使用 `dsm-package.yaml` 的 `compose.files`、`compose.web` 與
155
+ `identity_gateway`;不使用 file/targets/address_env 或手寫 upstream。
156
+ 省略 `ui.launch` 時預設 `new-page`,使用 SDK standalone launcher,不帶 Application Portal。
157
+ Headless Compose 不產生 launcher。health 必須選實際 endpoint 與 2xx status;
158
+ readiness 不跟隨 redirect,check 不會發 HTTP。Compose 不支援 ctl。
165
159
 
166
160
  For controlling installed Apps or designing task-level AI commands, read [reference/ctl.md](reference/ctl.md). Use ctl host list, register/renew explicitly, discover all commands, and always name the host when agents may operate multiple NAS machines.
@@ -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.2
26
+ sdk: 0.17.0
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 uses SDK/templates 0.15.2 with CLI 0.15.4. Use the published, matching versions together; the legacy packaging route and bridge publish mode are removed.
154
+ The unified Compose contract uses SDK 0.17.0 and templates 0.17.1 with CLI 0.17.0. 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
 
@@ -187,13 +187,28 @@ bridge-unverified admission blocker is replaced by `COMPOSE_READINESS_INVALID`,
187
187
 
188
188
  Compose SPKs intentionally omit `install_dep_packages=ContainerManager`: DSM can
189
189
  report that dependency not ready even while its systemd unit and Docker daemon
190
- are active. The official `docker-project` worker checks actual availability;
191
- this does not authorize starting, modifying or repairing Container Manager.
192
-
193
- For disposable environments, optional `compose.resource_prefix: t-0661-` prefixes
190
+ are active. The SDK's package-account preflight runs before install preparation
191
+ and before project startup. It reads fixed Container Manager package/executable
192
+ paths and the kernel's listening Unix-socket inventory; it never opens the Docker
193
+ socket, needs root, starts services or repairs Container Manager. Missing software
194
+ fails with `COMPOSE_CONTAINER_MANAGER_REQUIRED`; installed but unavailable or
195
+ unverifiable Docker fails with `COMPOSE_CONTAINER_MANAGER_NOT_READY`. Stable
196
+ message keys carry English defaults telling the user to install/start Container
197
+ Manager from Package Center. The install hook writes the DSM temporary error log
198
+ when provided; start returns the same error; first-open CGI displays an i18n-ready
199
+ 503 page even when the App gateway is stopped. This is availability evidence,
200
+ not Docker/project identity or a successful container API call. DSM wizard display
201
+ and permission behavior still need device acceptance.
202
+
203
+ Exclude NAS platforms which cannot run Container Manager through the package's
204
+ supported `arches`/INFO arch declarations. Do not advertise an unsupported
205
+ architecture merely because the App image has it; review model compatibility
206
+ within the declared family too.
207
+
208
+ For disposable environments, optional `compose.resource_prefix: sandbox-` prefixes
194
209
  all generated project/network/volume names while retaining the package ID as the
195
210
  namespace suffix. Keep it stable across upgrades: changing it selects new
196
- resources and does not migrate or delete retained data.
211
+ resources and does not move or delete retained data.
197
212
 
198
213
  Bridge web ingress uses a dedicated `sdk_ingress` internal bridge. The web service
199
214
  receives a fixed IPv4 address, and the package-account gateway connects directly
@@ -204,7 +219,7 @@ still adds the existing outbound bridge; other App services retain their interna
204
219
 
205
220
  The subnet is a deterministic /29 candidate derived from package ID and
206
221
  `compose.network.ingress_candidate` (integer 0-255, default 0). It comes from
207
- private 10.128.0.0/9; this pool is not globally reserved. Before acquisition, the
222
+ the upper half of the RFC 1918 10/8 private address block; this pool is not globally reserved. Before acquisition, the
208
223
  package-account preparer reads kernel IPv4 routes and all interface prefixes, and rejects overlap with
209
224
  `COMPOSE_SUBNET_CONFLICT`. Choose another candidate and rebuild; no silent
210
225
  renumbering occurs on upgrade. Choose a different candidate for a clean
@@ -347,3 +362,5 @@ verified loopback address setting, cannot remap ports or enforce isolated
347
362
  egress, and is not a general replacement for Header/native-cookie bridge Apps.
348
363
  Do not silently switch networking, add Docker socket access, run an App hook as
349
364
  root or delete DSM configuration to claim compatibility.
365
+
366
+ Compose web health is required before check/build; init has no guessed endpoint or status. Select both `--health-path` and `--health-status`, or edit `compose.web.health`. Readiness does not follow redirects. Static check is not an HTTP or arbitrary-UID/storage preflight. For official-image upstream packaging and DSM login, read [upstream-fork.md](upstream-fork.md).
@@ -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.2}
44
- backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.15.2, node: {package: Node.js_v20}}
43
+ backend: {runtime: python, dir: app, entry: main.py, sdk: 0.17.0}
44
+ backend: {runtime: node, dir: app, entry: main.mjs, sdk: 0.17.0, node: {package: Node.js_v20}}
45
45
  backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet]}
46
46
  ```
47
47
 
@@ -91,8 +91,8 @@ width、minWidth 是 320–3840,height、minHeight 是 240–2160,最小值
91
91
 
92
92
  ## 兩種層級
93
93
 
94
- - 無升權 App(沒有 rootOps、delegate;含所有 Compose App):安裝、執行、解除安裝都只用套件帳號,任何階段都不用 root。不能宣告 portal(native `STANDALONE_PORTAL_REQUIRES_EXECUTOR`、Compose `CONTRACT_PORTAL_REQUIRES_EXECUTOR`);需要登入入口別名時由管理員在 DSM 手動設定反向代理。解除安裝後 DSM 保留的「控制台」>「應用程式權限」規則由管理員手動移除,精靈與 cleanup 清單會說明。
95
- - executor App(rootOps 或 delegate):解除安裝時 root purge 自動刪除 `SYNO.SDS.<package>.Instance` 的應用程式權限規則,宣告 portal 時在 DSM 移除別名後 reload nginx。精靈說明自動清除;cleanup 清單只在自動清除失敗時(系統日誌 `synopkgland-executor`)才需要管理員處理。
94
+ - 無升權 App(沒有 rootOps、delegate;含所有 Compose App):安裝、執行、解除安裝都只用套件帳號,任何階段都不用 root。schema-2 App 的 os_min_ver 為 7.2-64570+ 時可以宣告 portal(含 unified Compose),由管理員明確套用別名,App 不會自動啟用或開啟入口;DSM 透過 INFO `instuninst_restart_services="nginx.service"` 處理解除安裝後的 nginx reload。DSM 7.1 仍需 executor 註冊入口,無升權 App 應提高最低 DSM 或省略 portal。解除安裝後 DSM 保留的「控制台」>「應用程式權限」規則由管理員手動移除,精靈與 cleanup 清單會說明。
95
+ - executor App(rootOps 或 delegate):解除安裝時 root purge 自動刪除 `SYNO.SDS.<package>.Instance` 的應用程式權限規則,Portal 的 nginx reload 由 DSM 依 INFO 執行,與 executor 核准分開。精靈說明自動清除;cleanup 清單只在自動清除失敗時(系統日誌 `synopkgland-executor`)才需要管理員處理。
96
96
 
97
97
  ## root 程式
98
98
 
@@ -1,10 +1,12 @@
1
1
  # DSM Application Portal
2
2
 
3
- Tier: only an App with the executor (`capabilities.rootOps` or `capabilities.delegate`) may declare a portal, because after uninstall only root can make nginx stop serving the alias. A no-privilege native App fails check/build with `STANDALONE_PORTAL_REQUIRES_EXECUTOR`, `add portal` refuses it unless the same call adds `root-ops` or `delegate`, and inspect flags such an SPK with `PORTAL_REQUIRES_EXECUTOR`. A Compose App has no executor: `integration.web.portal` always fails with `CONTRACT_PORTAL_REQUIRES_EXECUTOR`. Without the executor, an administrator can set up a reverse proxy in DSM instead.
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
- Declare: native-go/binary with rootOps or delegate, `portal: {}` or `portal: {alias: my-app}` in dsm-package.yaml (`synopkgland add root-ops portal`, or `add portal` once the executor is declared). 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 only `synopkgland_portal_alias="<alias>"` 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`.
5
+ 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
6
 
7
- Build (os_min_ver 7.2-64570+): the SPK carries `etc/portal.json` (every field DSM's app-portal worker needs) and `etc/portal.mustache`, `conf/resource` registers them, and the install wizard adds a prefilled alias step. preinst validates the answer; postinst stores it as the package's own draft and default alias. Nothing is active until an administrator applies it. `inspect <spk> --json` reports `portal: {appId, alias}` and refuses any other portal content.
7
+ 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`.
8
+
9
+ Build (os_min_ver 7.2-64570+): the SPK carries `etc/portal.json` (every field DSM's app-portal worker needs) and `etc/portal.mustache`, `conf/resource` registers them, and the install wizard adds a prefilled alias step. preinst validates the answer; postinst stores it as the package's own draft and default alias. Nothing is active until an administrator applies it. `inspect <spk> --json` reports `portal: {appId, alias}` and refuses any other portal content or a missing nginx reload declaration (`PORTAL_RELOAD_MISSING`).
8
10
 
9
11
  Login style: every portal build gives the launcher in `ui/config` a DSM `loginStyle`, which Control Panel > Login Portal > Applications draws as the entry's thumbnail and the alias login page uses for its wallpaper and button colour. Without it the thumbnail is blank. The CLI default is a neutral wallpaper, its thumbnail, a login-box mask and colour `#2F6BD8`; the launcher's `loginIcon` is the App icon, so the login page shows the icon beside the NAS name. Override any part in dsm-package.yaml:
10
12
 
@@ -24,15 +26,17 @@ Read: ordinary users call the Go backend's `portal.status` (`authenticated`), wh
24
26
 
25
27
  Executor mode (7.1-42661 ≤ os_min_ver < 7.2-64570): no web-config resource, no `etc/portal.*`, no alias wizard step or alias lifecycle. The executor policy carries `portal: {application: <displayname>, alias}` and the operations `portal.register` and `portal.unregister`; the executor writes `/usr/syno/etc/www/app.d/<package>.{json,mustache}`. The package name must match `^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$` (≤ 64) and displayname be 1–64 characters without control characters, or build fails with `STANDALONE_PORTAL_ENTRY_INVALID`. The native Go CGI is built with `-X main.portal=executor`; the Python/Node/Rust bridge CGI uses `-X main.portalMode=executor`: `portal.status` adds `entry` (`registered`, `unregistered`, `cleanup-required`; an unreadable executor is `unavailable`), and `portal.register` / `portal.unregister` take the administrator's DSM session and return the fresh status. `ctx.portal.register()` / `unregister()` call them; the plain panel shows text and "Enable entry" while unregistered, the alias form and "Remove entry" once registered, text and "Remove entry" when cleanup is required, and no entry buttons for non-administrators. Executor refusals keep their code (`PORTAL_FAILED`). Needs templates with the executor portal routes (after 0.9.4) and an SDK with the `portal` entry API (after 0.9.5).
26
28
 
27
- DSM floor: os_min_ver must be 7.1-42661 or later; below 7.2-64570 the executor registers the entry (above). DSM 7.1 marks the web-config worker Synology-only, so it refuses an unsigned SPK whose conf/resource carries web-config (Package Center error 320, "non-synology-signed package should not use internal worker"; seen on 7.1.1-42962). synopkg from DSM 7.2 on drops that mark, the only change to its worker table; the 7.2 floor comes from that source and is not yet verified on a 7.2 NAS. 7.3.2-86009 and 7.4 install it. Below 7.1 validate/build fail with `STANDALONE_PORTAL_MIN_DSM`, `--min-dsm` cannot go below 7.1, `add portal` raises a lower os_min_ver to 7.1-42661, and inspect flags an INFO os_min_ver below the floor of the SPK's mode. Fix: raise os_min_ver or remove the portal.
29
+ DSM floor: os_min_ver must be 7.1-42661 or later; below 7.2-64570 the executor registers the entry (above). DSM 7.1 marks the web-config worker Synology-only, so it refuses an unsigned SPK whose conf/resource carries web-config (Package Center error 320, "non-synology-signed package should not use internal worker"; seen on 7.1.1-42962). synopkg from DSM 7.2 on drops that mark, the only change to its worker table; the 7.2 floor comes from that source and is not yet verified on a 7.2 NAS. 7.3.2-86009 and 7.4 install it. Below 7.1 validate/build fail with `STANDALONE_PORTAL_MIN_DSM`, `--min-dsm` cannot go below 7.1, with an executor, `add portal` raises a lower os_min_ver to 7.1-42661; a no-executor target below 7.2-64570 is refused, and inspect flags an INFO os_min_ver below the floor of the SPK's mode. Fix: raise os_min_ver or remove the portal.
28
30
 
29
31
  Capability: beyond that floor, no version gate. The panel needs SYNO.API.Info to list `SYNO.Core.AppPortal` (v2) and the app's registration to exist; otherwise administrators get only the Control Panel shortcut.
30
32
 
31
33
  UI: plain JS calls `ctx.portal && ctx.portal.mountPanel(element)` and disposes it on unmount; Vue renders `portal/PortalSettings.vue`. `ctx.portal.messages` carries the SDK text in all nine languages.
32
34
 
33
- Never: write `AppPortal.json`, `@appconf/<package>` or `/usr/syno/etc/www/app.d` from the App (only the executor's approved `portal.*` operations write app.d); add a backend route that changes the alias; relay QuickConnect responses or build QuickConnect links; reboot or change other packages' portal entries while testing. DSM keeps the alias across upgrades and removes the entry on uninstall, but it does not reload nginx, and it keeps the App's Application Privileges rules.
35
+ Never: write `AppPortal.json`, `@appconf/<package>` or `/usr/syno/etc/www/app.d` from the App (only the executor's approved `portal.*` operations write app.d); add a backend route that changes the alias; relay QuickConnect responses or build QuickConnect links; reboot or change other packages' portal entries while testing. DSM keeps the alias across upgrades and removes the entry on uninstall. The INFO declaration requests nginx reload; DSM keeps the App's Application Privileges rules unless an executor removes those separately.
36
+
37
+ Uninstall: DSM removes the resource-worker Portal entry and reloads nginx through INFO. Reload is asynchronous and can finish after uninstall; the App needs no executor approval for this cleanup. If the alias still serves a DSM page after reload, an administrator can save an application alias in Control Panel > Login Portal > Applications to request another reload. Application Privileges are separate: an executor removes the App's rules when available; without one, an administrator removes retained rules there if the App will not be reinstalled. The wizard and `status`/`inspect` cleanup list describe these separately.
34
38
 
35
- Uninstall: the root executor's purge deletes the rules of `SYNO.SDS.<package>.Instance` through the Application Privileges API and, once DSM has removed the alias files, reloads nginx (`synosystemctl reload nginx`), so the alias stops at once. Failures are only logged to the system log entry `synopkgland-executor`; then an administrator saves an alias in Control Panel > Login Portal > Applications (which reloads nginx) or restarts the NAS, and removes the rules in Control Panel > Application Privileges. The uninstall wizard and the `status`/`inspect` cleanup list say this in that order: automatic first, manual only as the fallback.
39
+ Version evidence: Synology's [DSM 7 WordPress example](https://help.synology.com/developer-guide/examples/compile_web_package.html) declares `os_min_ver="7.0-40337"` with this INFO key; the [web-config guide](https://help.synology.com/developer-guide/resource_acquisition/web_config.html) requires it because the worker does not reload nginx itself. The key therefore adds no DSM 7.0/7.1 version floor; the remaining 7.2 floor for no-executor Portal is the unsigned worker restriction, not INFO support. Documentation confidence: high. End-to-end uninstall was verified on DSM 7.3.2 in a minimal Portal fixture; the 7.1/7.4/7.4.1 hardware matrix remains deferred. DSM 7.0 Portal registration remains unsupported.
36
40
 
37
41
  Executor mode uninstall: the purge reads the retained policy and unregisters the entry. If the executor is not running or was never approved, uninstall does not remove the entry; the wizard and cleanup list say so and give the root commands (`rm -f /usr/syno/etc/www/app.d/<package>.json /usr/syno/etc/www/app.d/<package>.mustache /usr/syno/etc/packages/<package>/AppPortal.json`, `synow3tool --disable-nginx-confd/--unreg-nginx-confd` for `dsm www alias .alias`, `--disable-nginx-sites/--unreg-nginx-sites` for `server .server` on `<c>.syno-app-portal.<package>.conf`, then `nginx -t && synow3tool --nginx=reload`).
38
42
 
@@ -61,6 +61,18 @@ Reuse the embedded theme's typography, spacing and public controls. Preserve
61
61
  loading, empty, error, disabled and permission states. Backend policies and DSM
62
62
  checks decide authority; component visibility cannot grant access.
63
63
 
64
+ ## Start fast
65
+
66
+ Every api.cgi call pays DSM's session check (about 60–240 ms on a NAS), so
67
+ the first screen waits for the startup chain, not the bundle:
68
+
69
+ - Run the entry gate first, then start the reads that do not depend on each
70
+ other together (`Promise.all`); do not chain them.
71
+ - Render `AppShell` as soon as the gate passes, with `:loading` while those
72
+ reads run: the sidebar and title show and the page body says it is loading.
73
+ - Start polling on its schedule; do not repeat at mount a read startup just
74
+ made. Delay background sampling that the first screen does not need.
75
+
64
76
  ## Verify the actual Vue app
65
77
 
66
78
  From `frontend`, run:
@@ -0,0 +1,165 @@
1
+ # Integrating an existing open-source App
2
+
3
+ Preserve the upstream fork, full history and `upstream` remote. Keep **every**
4
+ wrapper-owned manifest, adapter, script, notice and document in `synology/`.
5
+ Never edit upstream README, Dockerfile, source, config, workflows or ignore files.
6
+ Do not rewrite the App or treat it as an SDK component. Use `.git/info/exclude`
7
+ for local output when needed.
8
+
9
+ ## Container: official image and a matching source tag
10
+
11
+ Use the upstream **official image**, pinned as `repository:tag@sha256:digest`.
12
+ For a multi-architecture image, pin its index digest so Container Manager can
13
+ select the NAS architecture. Container Manager downloads the pinned image at
14
+ install. Retain the upstream source fork to match releases and support required
15
+ source/license delivery, including Corresponding Source where applicable. A
16
+ matching source tag alone does not prove a complete license/source inventory.
17
+
18
+ ```sh
19
+ git clone <your-fork-url> app
20
+ cd app
21
+ git remote add upstream <upstream-url>
22
+ synopkgland init -t compose --upstream-fork --package MyApp \
23
+ --upstream-image owner/app --health-path /actual-readiness --health-status 200 .
24
+ ```
25
+
26
+ Replace `owner/app` with the verified official repository. `init` does not guess
27
+ its publisher or make network requests. Configure `synology/upstream-recipe.json`:
28
+ `repository`, the actual Compose `service`, and `imageTag: tag` (exact upstream
29
+ tag) or `version` (strip the leading `v` only if the official image uses that
30
+ spelling). Adjust the App's port, storage and DSM login adapter before committing.
31
+ The initial `SELECT_UPSTREAM_TAG` marker must be replaced by a successful upgrade.
32
+
33
+ ```sh
34
+ bash synology/upgrade.sh v1.2.3 2
35
+ ```
36
+
37
+ The script requires a clean tree, fetches the exact upstream tag into a private
38
+ ref, verifies ancestry and untouched upstream files, then merges. It updates the
39
+ official image tag and calls the existing `synopkgland images pin` registry
40
+ resolver, recording `tag@digest` in `synology/compose.image.yaml`. That wrapper
41
+ Compose override is listed after the base file; upstream files remain unchanged.
42
+ The resolver requests OCI index/manifest-list media types. Verify the official
43
+ image supports every declared architecture; a single-platform manifest does not
44
+ become multi-architecture merely because it has a digest. A failed lookup leaves
45
+ the previous override intact and stops the upgrade.
46
+
47
+ Next, the script derives `version: 1.2.3` and explicit increasing packaging build
48
+ `0002`, records source tag/commit/build in `upstream-source.json`, then runs
49
+ `check --json` and `build --json`. It performs no image compilation or publication.
50
+ Stable `[v]X.Y.Z` only. Build numbers increase globally (1–9999), even when upstream
51
+ versions change. Review/inspect the SPK and commit `synology/` before another
52
+ upgrade. Resolve or revert a failed merge explicitly; rehearse retained-data
53
+ upgrades separately. No Git push, Release, NAS installation or deployment occurs.
54
+
55
+ For fixed-UID image data directories, currently supply a startup adapter or an
56
+ upstream-supported writable path under the control of `synology/`. Verify the
57
+ package UID can create first-run config/cache/data and retain it across upgrades.
58
+ Do not use root/chown lifecycle hooks or substitute a fixed UID. Replace this
59
+ workaround with the framework ownership mechanism once available and verified.
60
+
61
+ ## Native: official release binaries or a source build hook
62
+
63
+ ```sh
64
+ synopkgland init -t native-go --ui plain --upstream-fork --package MyApp .
65
+ ```
66
+
67
+ Native Apps can stage official release binaries pinned by **SHA-256**, or compile
68
+ the merged tree with a source build hook. OpenList uses the official release
69
+ binary route: select the binary for the matching upstream tag and architecture,
70
+ verify its pinned SHA-256 before staging it, and retain the source fork for
71
+ version/source correspondence. Do not execute an unverified download.
72
+
73
+ Implement `synology/build-source.sh` as the native payload hook; it deliberately
74
+ fails until implemented. For official release binaries, use that hook to acquire,
75
+ verify and stage the pinned release payload. For a source build, compile the
76
+ merged source and its frontend with pinned toolchains/dependencies. The hook
77
+ receives `SPL_UPSTREAM_SOURCE`, `SPL_UPSTREAM_TAG`, `SPL_UPSTREAM_COMMIT`; stage
78
+ architecture-specific `daemon.targets` below `synology/` and keep the small SDK
79
+ CGI as a separate management shell. The upgrade command fetches/merges the tag,
80
+ derives the version, runs the selected native payload hook, checks and builds.
81
+
82
+ ## Use DSM login
83
+
84
+ Every integrated App must admit **DSM administrators directly**, without a separate
85
+ App password, an OIDC/SSO server, or upstream source changes. The scaffold declares
86
+ `entry: admin-only`; Compose also declares gateway `access: dsm-admins`. This gates
87
+ the entry but does not itself create an upstream session. Choose these supported
88
+ upstream mechanisms in order:
89
+
90
+ 1. **Trusted header / reverse-proxy authentication**: configure gateway header
91
+ mode and the upstream's documented proxy-auth setting. FreshRSS supports this:
92
+ use its supported server-side `REMOTE_USER` configuration; the `synology/`
93
+ adapter may map admitted DSM administrators to a fixed shared upstream admin.
94
+ Clear incoming competing identity headers and admit upstream traffic only
95
+ through the gateway's dedicated bridge. Document shared data/audit identity.
96
+ 2. **Disable native login / allow guests**: configure the supported option and
97
+ let the DSM administrator gateway protect the sole reachable entry.
98
+ Uptime Kuma's `disableAuth` is the example. First bootstrap must complete native setup,
99
+ enable `disableAuth` via the upstream UI/API and read the state back before
100
+ declaring ready. An app-specific `synology/` adapter should generate any
101
+ required one-time setup secret, use it only for bootstrap and clear it; do not
102
+ ask users to remember/type an additional App password or edit the DB. The
103
+ existing prototype's setup rehearsal is not a generic shipped bootstrap SDK.
104
+ If manual App login is still necessary, label bootstrap incomplete. Verify
105
+ subsequent DSM entry, restart and upgrade all retain password-free access,
106
+ while anonymous/nonadmin requests and direct upstream ports remain blocked.
107
+ 3. **Existing SDK token adapter**: SDK **0.17.0** supports `mode: token`, with
108
+ `access: dsm-admins` and `entry: admin-only`. OpenList uses its unmodified
109
+ upstream `admin token` CLI while the server is stopped to obtain the native
110
+ administrator API token. The package stores it in a package-private **0600**
111
+ file under a 0700 directory. After DSM administrator admission, the gateway
112
+ runs the package's token adapter and injects `Authorization` server-side,
113
+ replacing client credentials. Packaging never delivers the token to the
114
+ browser, URL, localStorage or page script. No new SDK authentication API,
115
+ OIDC/SSO server, upstream source change or direct database edit is required.
116
+ All admitted DSM administrators share one upstream admin account; this does
117
+ not provide per-user isolation. After OpenList **Reset Token**, restart the
118
+ package to refresh the cached credential; failure refuses startup/requests
119
+ rather than falling back to anonymous access. WebDAV retains its own native
120
+ credentials: API-token injection does not make WebDAV password-free. The
121
+ upstream admin's own token/settings APIs remain unchanged, so an admitted
122
+ administrator may still inspect or rotate the native token through them.
123
+ Recheck `admin token`, API header and frontend `/api/me` behavior on upgrades;
124
+ verify actual DSM admission/revocation separately. The OpenList authentication
125
+ adapter is implemented and tested with the existing SDK; generic provisioning
126
+ automation and complete device acceptance are separate work.
127
+
128
+ ## Health, prerequisites and prototype boundaries
129
+
130
+ Choose the actual unauthenticated health endpoint and its expected 2xx status
131
+ with both init flags, or fill `compose.web.health` before check/build. No guessed
132
+ `/` + 200. Readiness **does not follow redirects**: a 302 to a dashboard/login is
133
+ unhealthy even if its destination is 200. A favicon/robots probe proves HTTP
134
+ availability only; verify database setup and full readiness separately. `check`
135
+ is static, not an HTTP or UID/storage probe. Compose ctl is rejected at check;
136
+ dedicated origin `/` is supported, automatic subpath rewriting is not.
137
+
138
+ Public npm availability does not grant anonymous Go SDK access. Obtain authorised
139
+ source access or an exact matching module cache with original checksums, or use
140
+ `SYNOPKGLAND_SDK_DIR` with an authorised matching checkout. Go source distribution
141
+ is undecided. Never copy credentials into generated projects.
142
+
143
+ | Prototype | Reusable lesson and remaining boundary |
144
+ | --- | --- |
145
+ | OpenList | Official release binary pinned by SHA-256, `/ping` and SDK 0.17.0 server-side token adapter; shared upstream admin, restart after token reset, native WebDAV credentials. Share selection does not grant package ACL/register Local storage. |
146
+ | FreshRSS | Official image, package-UID Apache/config adapter in `synology/`, explicit health; supported proxy authentication with fixed admin mapping and header clearing. |
147
+ | Uptime Kuma | Official image, writable-path adapter, nonredirecting health and `disableAuth` after first bootstrap; WebSocket expiry/revocation need separate tests. |
148
+
149
+ Historical bounded Linux/prototype proofs are not DSM acceptance. Recheck the
150
+ selected official image, arbitrary UID, persistent data, bootstrap and login
151
+ boundaries; preserve complete third-party notices and source correspondence.
152
+
153
+ ## Container Manager prerequisite
154
+
155
+ Container SPKs omit `install_dep_packages=ContainerManager` because DSM dependency
156
+ status can report not-ready while Docker is active. SDK preflight checks fixed
157
+ Container Manager files plus a listening Docker socket in the kernel inventory,
158
+ as the package UID without socket/API access or root. It fails before install
159
+ preparation/project startup with stable translated-message keys and an English
160
+ instruction to install/start Container Manager from Package Center. DSM's supplied
161
+ installer log, start failure and first-open CGI carry that diagnosis; the CGI
162
+ returns 503 even when the gateway is down. Availability is not identity attestation.
163
+ Declare only Container Manager-capable `arches`/INFO platforms; exclude unsupported
164
+ NAS architectures and review models in each family. Actual missing-package wizard
165
+ behavior and DSM's dependency not-ready behavior remain device acceptance items.
@@ -3,7 +3,7 @@
3
3
  只使用 schema-2 `dsm-package.yaml`。先讀 [compose.md](compose.md) 的宣告、支援表及 DSM 阻擋條件,再讀 [identity-gateway.md](identity-gateway.md) 決定 none/header/token/OIDC。不架 SSO Server,也不需要假 backend 或 wrapper container。
4
4
 
5
5
  ```sh
6
- synopkgland init -t compose --from ./compose.yaml my-app
6
+ synopkgland init -t compose --from ./compose.yaml --health-path /actual-readiness --health-status 200 my-app
7
7
  synopkgland check --json
8
8
  synopkgland build --arch x86_64
9
9
  synopkgland inspect dist/MyApp-x86_64.spk --json
@@ -14,3 +14,5 @@ synopkgland inspect dist/MyApp-x86_64.spk --json
14
14
  容器使用固定套件身分,不繼承登入者 File Station ACL。Shared Folder 需 composeMounts 明確 opt-in,安裝後由 admin 確認;官方 protected ACL/project control 未驗證前只啟動 SDK 設定頁,不 acquire 上游,不用 host root 繞過。每使用者檔案權限需 session/Broker Adapter,本次不做。
15
15
 
16
16
  交付 inspect 的 upstream/effective hashes、image digests、run-as、mount、egress 與 acceptance blockers。Linux 通過不代表 DSM 已驗收。部署請讀 [deploy.md](deploy.md),保留資料並由專用 thread 驗 final SPK digest。
17
+
18
+ For an upstream fork, use [the upstream-fork recipe](upstream-fork.md) and `--upstream-fork` to keep all wrapper files in `synology/`. Container forks require `--upstream-image <official-repository>` and use official tag+digest images; native forks stage official release binaries pinned by SHA-256 or use a source build hook. Health flags require an actual App endpoint and expected 2xx response; readiness does not follow redirects.