@synopackageland/cli 0.9.3 → 0.9.5

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 (35) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +1 -1
  3. package/dist/add.js +3 -3
  4. package/dist/build-spk/dsm-version.d.ts +14 -4
  5. package/dist/build-spk/dsm-version.js +17 -5
  6. package/dist/build-spk/uninstall-wizard.d.ts +2 -0
  7. package/dist/build-spk/uninstall-wizard.js +7 -1
  8. package/dist/build-spk/wizard-messages.d.ts +11 -0
  9. package/dist/build-spk/wizard-messages.js +11 -0
  10. package/dist/error-registry.js +2 -1
  11. package/dist/info.js +2 -1
  12. package/dist/portal/alias.d.ts +8 -0
  13. package/dist/portal/alias.js +16 -0
  14. package/dist/runtime/executor-digests.json +22 -0
  15. package/dist/runtime/executor-policy.d.ts +8 -0
  16. package/dist/runtime/executor-policy.js +29 -5
  17. package/dist/runtime/uninstall-cleanup.d.ts +5 -1
  18. package/dist/runtime/uninstall-cleanup.js +11 -1
  19. package/dist/standalone/build.js +3 -3
  20. package/dist/standalone/dsm-window.js +16 -3
  21. package/dist/standalone/inspect-spk.js +8 -4
  22. package/dist/standalone/manifest.js +7 -3
  23. package/dist/standalone/native-layout.d.ts +3 -0
  24. package/dist/standalone/native-layout.js +37 -5
  25. package/dist/standalone/native-root-window.d.ts +2 -1
  26. package/dist/standalone/native-root-window.js +15 -5
  27. package/dist/standalone/portal-window.d.ts +4 -2
  28. package/dist/standalone/portal-window.js +54 -12
  29. package/dist/standalone/portal.d.ts +4 -1
  30. package/dist/standalone/portal.js +8 -2
  31. package/dist/standalone/spk.js +2 -1
  32. package/package.json +3 -3
  33. package/skills/reference/portal.md +8 -4
  34. package/skills/reference/ui-json.md +5 -1
  35. package/skills/reference/ui-vue.md +7 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.5 (2026-10-12)
4
+
5
+ Builds with SDK 0.9.6 and templates 0.9.5: `init` writes SDK v0.9.6,
6
+ `@synopackageland/sdk` 0.9.6 and `@synopackageland/ui` 0.9.2. The executor
7
+ digest table records SDK 0.9.6, whose executor registers the portal entry
8
+ and checks a grant's background work from the browser address it recorded,
9
+ so a rebuilt App carries new executor digests. The policy digest changes
10
+ only for an App whose executor registers its portal entry (a portal and
11
+ os_min_ver below 7.2): after the upgrade it asks for approval again. Other
12
+ Apps keep their policy digest.
13
+
14
+ 繁中摘要:使用 SDK 0.9.6 與 templates 0.9.5(UI 0.9.2)。executor digest 表加入
15
+ SDK 0.9.6,重建的 App 會帶新的 executor digest。只有由 executor 註冊入口的
16
+ portal App(宣告 portal 且 os_min_ver 低於 7.2)policy digest 會變,升級後需重新
17
+ 核准;其他 App 的 policy digest 不變。DSM 7.1 起可用 portal(executor 模式)。
18
+
19
+ - The generated DSM window stays inside the desktop: its width is at most the
20
+ viewport's and its height at most the viewport's less the DSM taskbar. This
21
+ applies to the default 960×640 and to the size DSM restores from
22
+ `restoreSizePos`, which may have been saved on a larger screen. Position and
23
+ maximized state are restored as before.
24
+ - Native root authorization (`createRootAccess` from
25
+ `@synopackageland/cli/authorization-sdk`): the first `run` after an `authorize`
26
+ runs the task that `authorize` just read back as exactly ours, instead of
27
+ reading the TaskScheduler list and task again. Any later run, a repair, and
28
+ a run after `remove` still read DSM first; `authorize` still reads the task
29
+ back after creating it and checks its identity and script.
30
+ - Native root authorization polls the task history every 0.5 s, then at most
31
+ once a second (was up to every 2 s), so a finished root task is seen up to
32
+ 1 s sooner.
33
+ - A portal works from DSM 7.1-42661 on. Below 7.2-64570 (executor mode) the
34
+ SPK carries no web-config resource, `etc/portal.*`, alias wizard step or
35
+ alias lifecycle; the executor policy carries `portal: {application, alias}`
36
+ and the `portal.register` / `portal.unregister` operations, and the CGI is
37
+ built with `-X main.portal=executor`. A package name or displayname the
38
+ executor would refuse fails with `STANDALONE_PORTAL_ENTRY_INVALID`; below
39
+ 7.1 `STANDALONE_PORTAL_MIN_DSM` still refuses the portal, and `add portal`
40
+ raises os_min_ver only to 7.1-42661. inspect reports the policy's entry
41
+ and flags an INFO os_min_ver below 7.1. Needs templates 0.9.5 and SDK 0.9.6.
42
+ - `ctx.portal` in executor mode: `status()` carries `entry`, administrators
43
+ get `register()` and `unregister()`, and the plain panel offers "Enable
44
+ entry", "Remove entry" or the alias form by entry state. New texts in nine
45
+ languages, including `PORTAL_FAILED`. The panel's Korean texts use the
46
+ formal -십시오 register.
47
+ - The uninstall wizard and the cleanup list of an executor-mode portal App
48
+ warn that a stopped or never-approved executor leaves the entry, and give
49
+ the root cleanup commands.
50
+ - native-layout: `portal: {}` or `{alias}` (DSM 7.1-42661+) writes
51
+ `synopkgland_portal_alias` into INFO for the product's own root helper;
52
+ inspect checks it against `native-layout.json`.
53
+ - DSM's web-config portal registration now needs `os_min_ver` 7.2-64570 or
54
+ later instead of 7.3.2-86009. DSM 7.1's package manager marks the web-config
55
+ worker Synology-only and refuses it in an unsigned SPK (error 320); synopkg
56
+ from DSM 7.2 on drops that mark, the only change to its worker table. The
57
+ 7.2 floor comes from that source and is not yet verified on a 7.2 NAS.
58
+ inspect flags a web-config portal SPK whose INFO os_min_ver is lower, with
59
+ this reason.
60
+
61
+ ## 0.9.4 (2026-10-12)
62
+
63
+ Builds with SDK 0.9.5 and templates 0.9.4: `init` writes SDK v0.9.5 and
64
+ `@synopackageland/sdk` 0.9.5, and the Vue App's `AppRoot` lets Dialogs leave
65
+ a DSM window. The executor digest table records SDK 0.9.5, whose executor
66
+ equals 0.9.4's; the policy digest stays the same, so an installed App needs
67
+ no new approval. AI guidance for DSM-window Dialogs matches the Vue App.
68
+
69
+ 繁中摘要:使用 SDK 0.9.5 與 templates 0.9.4。SDK 0.9.5 只改 TypeScript(NAS
70
+ picker 改用 UI 0.9.1 元件),executor digest 與 0.9.4 相同,policy digest 不變,
71
+ 已安裝的 App 不需重新核准。ui-vue 與 ui-json 指引改為在 DSM 視窗內給 `AppRoot`
72
+ `dialog-bounds="viewport"`,並把 `activate` 接到 `ctx.host.activateWindow()`:
73
+ Dialog 可拖出 App 視窗,點 Dialog 會把視窗帶到最前;600px 以下為底部 sheet。
74
+
75
+ - `ui-vue` and `ui-json`: in a DSM window (`ctx.host.platform().host === 'dsm'`),
76
+ give `AppRoot` `dialog-bounds="viewport"` and connect its `activate` event to
77
+ `ctx.host.activateWindow()`, so Dialogs can be dragged out of the window and
78
+ pressing one brings the window to the front. Below 600px they are bottom
79
+ sheets. Standalone web, Portal and previews keep Dialogs inside the App.
80
+ - A host-window test pins that the mounted App's `activateWindow` fronts only
81
+ its own DSM window.
82
+
3
83
  ## 0.9.3 (2026-10-12)
4
84
 
5
85
  Builds with SDK 0.9.4 and templates 0.9.3: `init` writes SDK v0.9.4 and
package/README.md CHANGED
@@ -72,7 +72,7 @@ Add them with `init --with …` or later with `synopkgland add <capability…>`.
72
72
  | `login` | Sign users in with their DSM account, without the DSM desktop | Nobody; requires HTTPS for password sign-in |
73
73
  | `delegate` | Move files into a user's chosen folder while the user is offline | An administrator, once |
74
74
  | `root-ops` | Run fixed `root/*.sh` scripts for administrators | An administrator, once |
75
- | `portal` | Open the app from its own Application Portal alias | An administrator sets the alias; needs `root-ops` or `delegate` and DSM 7.3.2-86009+ |
75
+ | `portal` | Open the app from its own Application Portal alias | An administrator sets the alias; needs `root-ops` or `delegate` and DSM 7.1-42661+ (below 7.2-64570 the executor registers the entry) |
76
76
 
77
77
  Delegate is implemented; its default operation is `files.moveFromStaging`.
78
78
  Device lifecycle acceptance on real hardware is still pending for delegate and root operations.
package/dist/add.js CHANGED
@@ -6,7 +6,7 @@ import { capabilityNames, capabilitySettings } from './capabilities.js';
6
6
  import { initFromTemplate, nativeCapabilitySettings } from './init.js';
7
7
  import { templatesRoot } from './template-paths.js';
8
8
  import { borrowedTarget, capabilityOverlays } from './capability-overlays.js';
9
- import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_MIN_DSM } from './build-spk/dsm-version.js';
9
+ import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM } from './build-spk/dsm-version.js';
10
10
  import { loadStandaloneManifest } from './standalone/manifest.js';
11
11
  import { PORTAL_REQUIRES_EXECUTOR } from './portal/alias.js';
12
12
  function files(root, dir = root) {
@@ -51,7 +51,7 @@ function withNotes(result, notes) {
51
51
  }
52
52
  /**
53
53
  * `portal: {}` in dsm-package.yaml; the Go and binary starters already carry the UI and backend side.
54
- * A lower os_min_ver is raised to PORTAL_MIN_DSM, the oldest DSM verified to install a portal.
54
+ * A lower os_min_ver is raised to PORTAL_EXECUTOR_MIN_DSM; below PORTAL_MIN_DSM the executor registers the entry.
55
55
  */
56
56
  function addPortal(root, result) {
57
57
  const loaded = loadStandaloneManifest(root);
@@ -64,7 +64,7 @@ function addPortal(root, result) {
64
64
  const manifestPath = join(root, 'dsm-package.yaml');
65
65
  const doc = parseDocument(readFileSync(manifestPath, 'utf8'));
66
66
  doc.set('portal', doc.createNode({}, { flow: true }));
67
- const notes = raiseOsMinVer(doc, loaded.manifest.os_min_ver, PORTAL_MIN_DSM, 'portal');
67
+ const notes = raiseOsMinVer(doc, loaded.manifest.os_min_ver, PORTAL_EXECUTOR_MIN_DSM, 'portal');
68
68
  writeFileSync(manifestPath, String(doc));
69
69
  return withNotes({ ...result, added: [...result.added, 'portal'] }, notes);
70
70
  }
@@ -8,11 +8,21 @@ export declare const DEFAULT_MIN_DSM: string;
8
8
  export declare const CONTAINER_MIN_DSM: string;
9
9
  /**
10
10
  * DSM registers an Application Portal through the package's web-config
11
- * app-portal resource. DSM 7.1.1-42962 refuses that resource in an unsigned
12
- * package (error 320, "internal worker"); DSM 7.3.2-86009 and 7.4 install it.
13
- * 7.2 and 7.3.0/7.3.1 are untested, so the floor is the oldest verified build.
11
+ * app-portal resource. DSM 7.1's package manager marks the web-config worker
12
+ * Synology-only and refuses it in an unsigned package (error 320, "internal
13
+ * worker"); synopkg from DSM 7.2 on does not. That 7.2 floor comes from the
14
+ * source and is not yet verified on a 7.2 NAS.
14
15
  */
15
- export declare const PORTAL_MIN_DSM = "7.3.2-86009";
16
+ export declare const PORTAL_MIN_DSM: string;
17
+ /** Why DSM below PORTAL_MIN_DSM refuses a package that declares the web-config portal resource. */
18
+ export declare const PORTAL_MIN_DSM_REASON = "DSM 7.1 \u628A web-config \u5217\u70BA Synology \u5C08\u7528 worker\uFF0C\u4EE5\u932F\u8AA4 320 \u62D2\u88DD\u5E36 portal\uFF08web-config app-portal\uFF09\u7684\u672A\u7C3D\u7AE0\u5957\u4EF6\uFF1B7.2 \u8D77\u958B\u653E\u662F\u4F9D synopkg \u539F\u59CB\u78BC\u5224\u65B7\uFF0C\u5C1A\u672A\u5BE6\u6A5F\u9A57\u8B49";
19
+ /**
20
+ * Below {@link PORTAL_MIN_DSM} the root executor registers the portal entry in
21
+ * DSM's app.d instead; that file format is verified from DSM 7.1 on.
22
+ */
23
+ export declare const PORTAL_EXECUTOR_MIN_DSM: string;
24
+ /** `true` when a portal package for `osMinVer` has its executor register the entry instead of web-config. */
25
+ export declare function portalByExecutor(osMinVer: string): boolean;
16
26
  /** Why a portal package cannot declare `value`, and how to fix it; `field` names the portal key. */
17
27
  export declare function portalMinDsmMessage(value: string, field: string): string;
18
28
  /** `true` when `value` is not older than `floor`, comparing version parts then build. */
@@ -15,14 +15,26 @@ export const DEFAULT_MIN_DSM = FIRST_BUILD["7.1"];
15
15
  export const CONTAINER_MIN_DSM = FIRST_BUILD["7.2.1"];
16
16
  /**
17
17
  * DSM registers an Application Portal through the package's web-config
18
- * app-portal resource. DSM 7.1.1-42962 refuses that resource in an unsigned
19
- * package (error 320, "internal worker"); DSM 7.3.2-86009 and 7.4 install it.
20
- * 7.2 and 7.3.0/7.3.1 are untested, so the floor is the oldest verified build.
18
+ * app-portal resource. DSM 7.1's package manager marks the web-config worker
19
+ * Synology-only and refuses it in an unsigned package (error 320, "internal
20
+ * worker"); synopkg from DSM 7.2 on does not. That 7.2 floor comes from the
21
+ * source and is not yet verified on a 7.2 NAS.
21
22
  */
22
- export const PORTAL_MIN_DSM = "7.3.2-86009";
23
+ export const PORTAL_MIN_DSM = FIRST_BUILD["7.2"];
24
+ /** Why DSM below PORTAL_MIN_DSM refuses a package that declares the web-config portal resource. */
25
+ export const PORTAL_MIN_DSM_REASON = "DSM 7.1 把 web-config 列為 Synology 專用 worker,以錯誤 320 拒裝帶 portal(web-config app-portal)的未簽章套件;7.2 起開放是依 synopkg 原始碼判斷,尚未實機驗證";
26
+ /**
27
+ * Below {@link PORTAL_MIN_DSM} the root executor registers the portal entry in
28
+ * DSM's app.d instead; that file format is verified from DSM 7.1 on.
29
+ */
30
+ export const PORTAL_EXECUTOR_MIN_DSM = FIRST_BUILD["7.1"];
31
+ /** `true` when a portal package for `osMinVer` has its executor register the entry instead of web-config. */
32
+ export function portalByExecutor(osMinVer) {
33
+ return !isDsmAtLeast(osMinVer, PORTAL_MIN_DSM);
34
+ }
23
35
  /** Why a portal package cannot declare `value`, and how to fix it; `field` names the portal key. */
24
36
  export function portalMinDsmMessage(value, field) {
25
- return `${field} 需要 DSM ${PORTAL_MIN_DSM} 以上,os_min_ver 是「${value}」。DSM 7.1.1 以錯誤 320 拒裝帶 portal(web-config app-portal)的未簽章套件;DSM 7.3.2 與 7.4 已驗證可裝,7.2 未驗證。請把 os_min_ver 提高到 ${PORTAL_MIN_DSM} 以上,或移除 ${field}。`;
37
+ return `${field} 需要 DSM ${PORTAL_EXECUTOR_MIN_DSM} 以上,os_min_ver 是「${value}」。DSM ${PORTAL_MIN_DSM} 以前由 executor 寫入 DSM 的 app.d 註冊入口,這個格式只在 DSM 7.1 起驗證過。請把 os_min_ver 提高到 ${PORTAL_EXECUTOR_MIN_DSM} 以上,或移除 ${field}。`;
26
38
  }
27
39
  /** `true` when `value` is not older than `floor`, comparing version parts then build. */
28
40
  export function isDsmAtLeast(value, floor) {
@@ -6,6 +6,8 @@ interface UninstallWizardOptions {
6
6
  executor?: boolean;
7
7
  /** The App declares a Login Portal alias (web-config app-portal); only the executor tier can. */
8
8
  portal?: boolean;
9
+ /** Below DSM 7.2 the executor registered the App's Application Portal entry. */
10
+ portalEntry?: boolean;
9
11
  }
10
12
  /** `uninstall_uifile` (English) plus one `uninstall_uifile_<dsm language>` per locale. */
11
13
  export declare function renderUninstallWizardFiles(packageName: string, kind: UninstallWizardKind, options?: UninstallWizardOptions): Record<string, string>;
@@ -1,10 +1,12 @@
1
+ import { portalEntryCleanupCommands } from "../runtime/uninstall-cleanup.js";
1
2
  import { renderWizardFiles } from "./wizard-i18n.js";
2
3
  /** The key DSM's built-in packages use for their "also remove my data" box. */
3
4
  export const REMOVE_ADDITIONAL_KEY = "pkgwizard_remove_additional";
4
5
  function list(items) {
5
6
  return `<ul class="synopkg-ul-list">${items.map((item) => `<li>${item}</li>`).join("")}</ul>`;
6
7
  }
7
- function renderUninstallWizard(t, packageName, kind, { executor = false, portal = false }) {
8
+ const html = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9
+ function renderUninstallWizard(t, packageName, kind, { executor = false, portal = false, portalEntry = false }) {
8
10
  const vars = { package: packageName };
9
11
  return [{
10
12
  step_title: t("uninstall.step"),
@@ -23,6 +25,10 @@ function renderUninstallWizard(t, packageName, kind, { executor = false, portal
23
25
  ...(executor ? [{ desc: t("uninstall.executor", vars) }] : []),
24
26
  { desc: t(executor ? "uninstall.appPrivilege.auto" : "uninstall.appPrivilege", vars) },
25
27
  ...(executor && portal ? [{ desc: t("uninstall.portal.auto", vars) }] : []),
28
+ ...(executor && portalEntry ? [
29
+ { desc: t("uninstall.portalEntry", vars) },
30
+ { desc: portalEntryCleanupCommands(packageName).map((line) => `<code>${html(line)}</code>`).join("<br>") },
31
+ ] : []),
26
32
  { desc: t("uninstall.backup") },
27
33
  {
28
34
  type: "multiselect",
@@ -417,6 +417,17 @@ export declare const WIZARD_MESSAGES: {
417
417
  es: string;
418
418
  it: string;
419
419
  };
420
+ "uninstall.portalEntry": {
421
+ en: string;
422
+ "zh-TW": string;
423
+ "zh-CN": string;
424
+ ja: string;
425
+ ko: string;
426
+ de: string;
427
+ fr: string;
428
+ es: string;
429
+ it: string;
430
+ };
420
431
  "uninstall.keep.native": {
421
432
  en: string;
422
433
  "zh-TW": string;
@@ -417,6 +417,17 @@ export const WIZARD_MESSAGES = {
417
417
  es: "Después de que DSM elimine el alias del Portal de inicio de sesión de {package}, la desinstalación recarga automáticamente el servidor web, de modo que el alias deja de funcionar al instante. Solo si esta recarga falla, como indica la entrada synopkgland-executor del registro del sistema, un administrador debe guardar un alias de aplicación en Panel de control > Portal de inicio de sesión > Aplicaciones, lo que recarga el servidor web, o reiniciar el NAS.",
418
418
  it: "Dopo che DSM ha rimosso l’alias del Portale di accesso di {package}, la disinstallazione ricarica automaticamente il server web, così l’alias smette subito di funzionare. Solo se questo ricaricamento non riesce, come segnala la voce synopkgland-executor del registro di sistema, un amministratore deve salvare un alias di applicazione in Pannello di controllo > Portale di accesso > Applicazioni, che ricarica il server web, oppure riavviare il NAS.",
419
419
  },
420
+ "uninstall.portalEntry": {
421
+ en: "Uninstall asks the root executor to remove the Application Portal entry of {package}. If the executor is not running or was never approved, uninstall does not remove the entry; an administrator then runs these commands as root:",
422
+ "zh-TW": "解除安裝會由 root executor 移除 {package} 的應用程式入口。如果 executor 未執行或從未核准,解除安裝不會移除應用程式入口,管理員需以 root 執行下列指令:",
423
+ "zh-CN": "卸载会由 root executor 移除 {package} 的应用程序门户入口。如果 executor 未运行或从未核准,卸载不会移除应用程序门户入口,管理员需以 root 执行下列命令:",
424
+ ja: "アンインストール時に root executor が {package} のアプリケーションポータルのエントリを削除します。executor が実行されていない、または承認されていない場合、アンインストールではエントリが削除されないため、管理者が root で次のコマンドを実行してください:",
425
+ ko: "제거 시 root executor가 {package}의 응용 프로그램 포털 항목을 제거합니다. executor가 실행 중이 아니거나 승인된 적이 없으면 제거 과정에서 항목이 제거되지 않으므로 관리자가 root로 다음 명령을 실행하십시오.",
426
+ de: "Bei der Deinstallation entfernt der Root-Executor den Eintrag von {package} im Anwendungsportal. Läuft der Executor nicht oder wurde er nie genehmigt, entfernt die Deinstallation den Eintrag nicht; ein Administrator führt dann diese Befehle als root aus:",
427
+ fr: "La désinstallation demande à l’executor root de supprimer l’entrée de {package} dans le Portail d’application. Si l’executor ne s’exécute pas ou n’a jamais été approuvé, la désinstallation ne supprime pas l’entrée ; un administrateur exécute alors ces commandes en tant que root :",
428
+ es: "La desinstalación pide al executor root que elimine la entrada de {package} del Portal de aplicaciones. Si el executor no se está ejecutando o nunca se aprobó, la desinstalación no elimina la entrada; un administrador debe ejecutar estos comandos como root:",
429
+ it: "La disinstallazione chiede all’executor root di rimuovere la voce di {package} dal Portale applicazioni. Se l’executor non è in esecuzione o non è mai stato approvato, la disinstallazione non rimuove la voce; un amministratore deve eseguire questi comandi come root:",
430
+ },
420
431
  "uninstall.keep.native": {
421
432
  en: "Never deleted: Shared Folders, the package account, and other apps' data.",
422
433
  "zh-TW": "不會被刪除:共用資料夾(Shared Folder)、套件帳號,以及其他 App 的資料。",
@@ -649,7 +649,8 @@ for (const [code, description, suggestion] of [
649
649
  ERROR_REGISTRY[code] = { code, description, suggestion };
650
650
  ERROR_REGISTRY["ADD_LAYOUT_UNSUPPORTED"] = { "code": "ADD_LAYOUT_UNSUPPORTED", "description": "SDK overlay 需要標準 backend 目錄。", "suggestion": "使用 backend.dir: backend,或手動合併 SDK overlay。" };
651
651
  ERROR_REGISTRY["MIN_DSM_CONTAINER_FLOOR"] = { "code": "MIN_DSM_CONTAINER_FLOOR", "description": "Compose 最低 DSM 版本過低。", "suggestion": "Compose 需要 DSM 7.2.1-69057 或以上。" };
652
- ERROR_REGISTRY.STANDALONE_PORTAL_MIN_DSM = { "code": "STANDALONE_PORTAL_MIN_DSM", "description": "宣告 portal 的套件 os_min_ver 低於 DSM 7.3.2-86009。", "suggestion": "DSM 7.1.1 以 320 拒裝帶 portal 的未簽章套件、7.2 未驗證;把 os_min_ver 提高到 7.3.2-86009 以上,或移除 portal。" };
652
+ ERROR_REGISTRY.STANDALONE_PORTAL_ENTRY_INVALID = { "code": "STANDALONE_PORTAL_ENTRY_INVALID", "description": "os_min_ver 低於 DSM 7.2-64570 時由 executor 註冊 portal,套件名稱或 displayname 不符合它的規則。", "suggestion": "套件名稱只能用英數字,以 . _ - 分隔,最多 64 字;displayname 1–64 個字,不可含控制字元。" };
653
+ ERROR_REGISTRY.STANDALONE_PORTAL_MIN_DSM = { "code": "STANDALONE_PORTAL_MIN_DSM", "description": "宣告 portal 的套件 os_min_ver 低於 DSM 7.1-42661。", "suggestion": "DSM 7.2-64570 以前由 executor 寫入 DSM 的 app.d 註冊入口,這個格式只在 DSM 7.1 起驗證過。把 os_min_ver 提高到 7.1-42661 以上,或移除 portal。" };
653
654
  ERROR_REGISTRY["MIN_DSM_INVALID"] = { "code": "MIN_DSM_INVALID", "description": "最低 DSM 版本格式不合法。", "suggestion": "指定 7.x release 或完整 7.x-build。" };
654
655
  ERROR_REGISTRY["NOTICE_UNSAFE"] = { "code": "NOTICE_UNSAFE", "description": "授權檔案不符合一般檔案限制。", "suggestion": "移除 symlink、特殊檔與超大授權檔案。" };
655
656
  ERROR_REGISTRY["NOTICE_MISSING"] = { "code": "NOTICE_MISSING", "description": "缺少第三方授權資訊。", "suggestion": "重新安裝模板依賴並產生 third-party notices。" };
package/dist/info.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { portalByExecutor } from "./build-spk/dsm-version.js";
3
4
  import { loadContract } from "./contract/load.js";
4
5
  import { appOriginFromContract } from "./host/open-url.js";
5
6
  import { resolveDeployIdentity } from "./deploy/identity.js";
@@ -169,7 +170,7 @@ export async function standaloneInfo(options) {
169
170
  ...state,
170
171
  capabilities,
171
172
  ...(executor ? { executor } : {}),
172
- uninstallCleanup: uninstallCleanup(m.package, { executor: Boolean(m.capabilities.rootOps || m.capabilities.delegate), portal: Boolean(m.portal) }),
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
174
  };
174
175
  }
175
176
  /** What the installed App's portal.status says; DSM owns the alias, an administrator sets it. */
@@ -25,3 +25,11 @@ export declare function isPortalAlias(value: unknown): value is string;
25
25
  export declare function defaultPortalAlias(packageId: string): string;
26
26
  /** `{}` or `{alias}`; undefined for any other shape. */
27
27
  export declare function readPortalDeclaration(value: unknown, packageId: string): PortalDeclaration | undefined;
28
+ /** The Application Portal entry a root executor registers in DSM's app.d, as its approved policy states it. */
29
+ export interface PortalEntry {
30
+ /** Control Panel display name. */
31
+ application: string;
32
+ alias: string;
33
+ }
34
+ /** Why the executor would refuse this entry; undefined when it accepts it. */
35
+ export declare function portalEntryProblem(packageId: string, entry: PortalEntry): string | undefined;
@@ -41,3 +41,19 @@ export function readPortalDeclaration(value, packageId) {
41
41
  return { alias: defaultPortalAlias(packageId) };
42
42
  return isPortalAlias(alias) ? { alias } : undefined;
43
43
  }
44
+ /**
45
+ * The executor's own rules (executor/portal_entry.go): the package name and
46
+ * alias become DSM file and nginx names, the application a JSON string.
47
+ */
48
+ const ENTRY_PACKAGE = /^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/;
49
+ const ENTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
50
+ /** Why the executor would refuse this entry; undefined when it accepts it. */
51
+ export function portalEntryProblem(packageId, entry) {
52
+ if (packageId.length > 64 || !ENTRY_PACKAGE.test(packageId))
53
+ return `套件名稱「${packageId}」不能當作 Application Portal 的檔名:只能用英數字,以 . _ - 分隔,最多 64 字。`;
54
+ if (!ENTRY_ALIAS.test(entry.alias))
55
+ return `portal 別名「${entry.alias}」不能當作 Application Portal 的路徑。`;
56
+ if (!entry.application || [...entry.application].length > 64 || /\p{Cc}/u.test(entry.application))
57
+ return `displayname「${entry.application}」不能當作 Application Portal 的名稱:1–64 個字,不可含控制字元。`;
58
+ return undefined;
59
+ }
@@ -53,5 +53,27 @@
53
53
  "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
54
54
  },
55
55
  "goToolchain": "go1.26.9"
56
+ },
57
+ "0.9.5": {
58
+ "x86_64": "fc3c206b6a8e30b7470cedabf6676c027960311469685eab2fe544fe6cc671ac",
59
+ "aarch64": "aa1a8a1b88758ad47ca69d30e9649a24a93b94b5b5fc181ce782035c2c77c497",
60
+ "armv7": "0c39cb98b67735fede6bbbe91471695e26707f377cbc50dd860a1d9a7fe06fcf",
61
+ "lifecycle": {
62
+ "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
63
+ "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
64
+ "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
65
+ },
66
+ "goToolchain": "go1.26.9"
67
+ },
68
+ "0.9.6": {
69
+ "x86_64": "4241dfebd2b819c2566eca062ee0021b99e7f86bbe4703f50833874f79b3cf54",
70
+ "aarch64": "923514aca8cf6576c6c52c737d53c2c7cc4bfcec40591dcec995005be41add95",
71
+ "armv7": "df959648ac8e6201f1cf9b86b57730c09ba1afb7ecc151ba2f530ad15dd90ccf",
72
+ "lifecycle": {
73
+ "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
74
+ "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
75
+ "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
76
+ },
77
+ "goToolchain": "go1.26.9"
56
78
  }
57
79
  }
@@ -1,8 +1,11 @@
1
1
  import type { RootOp } from '../standalone/root-ops.js';
2
2
  import type { EntryMode } from '../standalone/manifest.js';
3
+ import { type PortalDeclaration, type PortalEntry } from '../portal/alias.js';
3
4
  export declare const FILE_OPERATIONS: readonly ["files.list", "files.info", "files.createFolder", "files.checkPermission", "files.write", "files.read", "files.rename", "files.copy", "files.move", "files.delete"];
4
5
  /** Started File Station tasks are followed with these; they are implied, never declared. */
5
6
  export declare const FILE_TASK_OPERATIONS: readonly ["files.taskStatus", "files.taskCancel"];
7
+ /** An executor that registers the portal entry serves both, and only with a policy `portal`. */
8
+ export declare const PORTAL_OPERATIONS: readonly ["portal.register", "portal.unregister"];
6
9
  export interface DelegateCapability {
7
10
  operations: string[];
8
11
  scope: string;
@@ -21,6 +24,8 @@ export interface ExecutorPolicy {
21
24
  lifetime?: 'until-revoked';
22
25
  package: string;
23
26
  operations: string[];
27
+ /** Below DSM 7.2 the executor registers this Application Portal entry; DSM 7.1 refuses web-config for unsigned packages. */
28
+ portal?: PortalEntry;
24
29
  root: Record<string, string>;
25
30
  }
26
31
  /** The exact policy file bytes whose sha256 is the policy pin. */
@@ -34,6 +39,9 @@ export declare function executorPolicy(m: {
34
39
  rootOps?: true;
35
40
  };
36
41
  rootOps?: RootOp[];
42
+ portal?: PortalDeclaration;
43
+ os_min_ver?: string;
44
+ displayname?: string;
37
45
  }): ExecutorPolicy;
38
46
  /** Inventory rejects malformed extensions even if a caller recomputes every pin. */
39
47
  export declare function validExecutorPolicy(value: unknown, pkg: string): value is ExecutorPolicy;
@@ -1,9 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { MOVE_FROM_STAGING, DELEGATE_EXTENSION_OPERATIONS } from './delegate-contract.js';
3
+ import { portalByExecutor } from '../build-spk/dsm-version.js';
4
+ import { portalEntryProblem } from '../portal/alias.js';
3
5
  export const FILE_OPERATIONS = ['files.list', 'files.info', 'files.createFolder', 'files.checkPermission', 'files.write', 'files.read', 'files.rename', 'files.copy', 'files.move', 'files.delete'];
4
6
  /** Started File Station tasks are followed with these; they are implied, never declared. */
5
7
  export const FILE_TASK_OPERATIONS = ['files.taskStatus', 'files.taskCancel'];
6
8
  const TASK_STARTING_OPERATIONS = ['files.copy', 'files.move', 'files.delete'];
9
+ /** An executor that registers the portal entry serves both, and only with a policy `portal`. */
10
+ export const PORTAL_OPERATIONS = ['portal.register', 'portal.unregister'];
7
11
  export function validDelegate(value) {
8
12
  if (!value || typeof value !== 'object' || Array.isArray(value))
9
13
  return false;
@@ -24,7 +28,8 @@ export const executorPolicyText = (policy) => JSON.stringify(policy) + '\n';
24
28
  export function executorPolicy(m) {
25
29
  if (m.capabilities.delegate && !validDelegate(m.capabilities.delegate))
26
30
  throw new Error('[EXECUTOR_POLICY] Invalid delegate operations or scope.');
27
- const operations = ['status'];
31
+ const portal = executorPortal(m);
32
+ const operations = ['status', ...(portal ? PORTAL_OPERATIONS : [])];
28
33
  if (m.capabilities.delegate) {
29
34
  operations.push('delegate.create', 'delegate.list', 'delegate.revoke', ...m.capabilities.delegate.operations);
30
35
  if (m.capabilities.delegate.operations.some(op => TASK_STARTING_OPERATIONS.includes(op)))
@@ -41,21 +46,33 @@ export function executorPolicy(m) {
41
46
  const d = m.capabilities.delegate;
42
47
  return { v: 1, package: m.package, app: `SYNO.SDS.${m.package}.Instance`, ...(m.entry ? { entry: m.entry } : {}), ...(d ? { scope: d.scope } : {}),
43
48
  ...(d?.lifetime ? { destinations: [...d.destinations].sort(), lifetime: d.lifetime } : {}),
44
- operations: [...new Set(operations)].sort(), root };
49
+ operations: [...new Set(operations)].sort(), ...(portal ? { portal } : {}), root };
50
+ }
51
+ /** The entry the executor registers: a portal App whose os_min_ver is below DSM 7.2. */
52
+ function executorPortal(m) {
53
+ if (!m.portal || !m.os_min_ver || !portalByExecutor(m.os_min_ver))
54
+ return undefined;
55
+ const entry = { application: m.displayname ?? '', alias: m.portal.alias };
56
+ const problem = portalEntryProblem(m.package, entry);
57
+ if (problem)
58
+ throw new Error(`[EXECUTOR_POLICY] ${problem}`);
59
+ return entry;
45
60
  }
46
61
  /** Inventory rejects malformed extensions even if a caller recomputes every pin. */
47
62
  export function validExecutorPolicy(value, pkg) {
48
63
  if (!value || typeof value !== 'object' || Array.isArray(value))
49
64
  return false;
50
65
  const p = value;
51
- if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'root'].includes(key))
66
+ if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'root'].includes(key))
52
67
  || p.v !== 1 || p.package !== pkg || p.app !== `SYNO.SDS.${pkg}.Instance`
53
68
  || 'entry' in p && p.entry !== 'admin-only' && p.entry !== 'app-privilege'
54
69
  || !Array.isArray(p.operations) || !p.operations.includes('status') || new Set(p.operations).size !== p.operations.length
55
70
  || !p.root || typeof p.root !== 'object' || Array.isArray(p.root))
56
71
  return false;
57
72
  const base = ['status', 'root.run', 'delegate.create', 'delegate.list', 'delegate.revoke'];
58
- if (p.operations.some(op => ![...base, ...FILE_OPERATIONS, ...FILE_TASK_OPERATIONS, ...DELEGATE_EXTENSION_OPERATIONS].includes(op)))
73
+ if (p.operations.some(op => ![...base, ...FILE_OPERATIONS, ...FILE_TASK_OPERATIONS, ...DELEGATE_EXTENSION_OPERATIONS, ...PORTAL_OPERATIONS].includes(op)))
74
+ return false;
75
+ if (!PORTAL_OPERATIONS.every(op => p.operations.includes(op) === (p.portal !== undefined)) || p.portal !== undefined && !validPortalEntry(p.portal, pkg))
59
76
  return false;
60
77
  const delegated = p.operations.some(op => op.startsWith('files.') || op.startsWith('delegate.'));
61
78
  if (!delegated)
@@ -65,5 +82,12 @@ export function validExecutorPolicy(value, pkg) {
65
82
  if (!validDelegate(capability))
66
83
  return false;
67
84
  const expected = executorPolicy({ package: pkg, capabilities: { delegate: capability, ...(p.operations.includes('root.run') ? { rootOps: true } : {}) } }).operations;
68
- return expected.length === p.operations.length && expected.every(op => p.operations.includes(op));
85
+ const declared = p.operations.filter(op => !PORTAL_OPERATIONS.includes(op));
86
+ return expected.length === declared.length && expected.every(op => declared.includes(op));
87
+ }
88
+ function validPortalEntry(value, pkg) {
89
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => key !== 'application' && key !== 'alias'))
90
+ return false;
91
+ const { application, alias } = value;
92
+ return typeof application === 'string' && typeof alias === 'string' && portalEntryProblem(pkg, { application, alias }) === undefined;
69
93
  }
@@ -9,5 +9,9 @@ export interface UninstallCleanupOptions {
9
9
  executor?: boolean;
10
10
  /** The App declares a Login Portal alias; only the executor tier can. */
11
11
  portal?: boolean;
12
+ /** Below DSM 7.2 the executor registered the App's Application Portal entry. */
13
+ portalEntry?: boolean;
12
14
  }
13
- export declare function uninstallCleanup(pkg: string, { executor, portal }?: UninstallCleanupOptions): UninstallCleanupItem[];
15
+ /** The root commands that remove an Application Portal entry the executor could not unregister. */
16
+ export declare function portalEntryCleanupCommands(pkg: string): string[];
17
+ export declare function uninstallCleanup(pkg: string, { executor, portal, portalEntry }?: UninstallCleanupOptions): UninstallCleanupItem[];
@@ -1,5 +1,14 @@
1
+ /** The root commands that remove an Application Portal entry the executor could not unregister. */
2
+ export function portalEntryCleanupCommands(pkg) {
3
+ return [
4
+ `rm -f /usr/syno/etc/www/app.d/${pkg}.json /usr/syno/etc/www/app.d/${pkg}.mustache /usr/syno/etc/packages/${pkg}/AppPortal.json`,
5
+ `for c in dsm www alias .alias; do n=$c.syno-app-portal.${pkg}.conf; synow3tool --disable-nginx-confd=$n; synow3tool --unreg-nginx-confd=$n; done`,
6
+ `for c in server .server; do n=$c.syno-app-portal.${pkg}.conf; synow3tool --disable-nginx-sites=$n; synow3tool --unreg-nginx-sites=$n; done`,
7
+ 'nginx -t && synow3tool --nginx=reload',
8
+ ];
9
+ }
1
10
  const AUTOMATIC_FAILED = 'Only if automatic cleanup failed after uninstall; see the system log entry synopkgland-executor.';
2
- export function uninstallCleanup(pkg, { executor = false, portal = false } = {}) {
11
+ export function uninstallCleanup(pkg, { executor = false, portal = false, portalEntry = false } = {}) {
3
12
  const items = [
4
13
  { target: `@appdata/${pkg}, @appconf/${pkg}, @apphome/${pkg}`, condition: 'After uninstall, if retained data or empty DSM directories are no longer needed.', action: 'Back up retained data; an administrator may remove only these package-specific directories. The delete-data checkbox empties their contents and preserves DSM directory roots.' },
5
14
  { target: `@appshare/${pkg}, /usr/syno/etc/packages/${pkg}`, condition: 'After uninstall, if DSM leaves package infrastructure or a package-config symlink.', action: 'Administrator: inspect these package-specific paths and remove unused infrastructure; do not follow the symlink to delete shared data.' },
@@ -8,6 +17,7 @@ export function uninstallCleanup(pkg, { executor = false, portal = false } = {})
8
17
  ? { target: `SYNO.SDS.${pkg}.Instance rules in Application Privileges`, condition: AUTOMATIC_FAILED, action: 'The root executor removes these rules automatically through the Application Privileges API on uninstall. Administrator: remove surviving rules in Control Panel > Application Privileges; DSM would apply them again to a later install under the same name.' }
9
18
  : { target: `SYNO.SDS.${pkg}.Instance rules in Application Privileges`, condition: 'After uninstall, if this package will not be reinstalled.', action: 'DSM keeps these rules in /etc/synoappprivilege.db and applies them again to a later install under the same name. Administrator: remove them in Control Panel > Application Privileges.' },
10
19
  ...(executor && portal ? [{ target: `${pkg} Login Portal alias`, condition: AUTOMATIC_FAILED, action: 'DSM removes the alias configuration without reloading its web server; the root executor then reloads the web server automatically. Administrator: if the alias still opens a DSM page, save an application alias in Control Panel > Login Portal > Applications to reload it, or restart the NAS.' }] : []),
20
+ ...(executor && portalEntry ? [{ target: `${pkg} Application Portal entry`, condition: 'After uninstall, if the executor was not running or was never approved, so it could not remove the entry.', action: `Administrator, as root: ${portalEntryCleanupCommands(pkg).join('; ')}` }] : []),
11
21
  { target: `${pkg} package account/group`, condition: 'After uninstall, only if this package will not be reinstalled and the identity is unused.', action: 'Administrator: review DSM internal account/group and associated ACLs before cleanup. Package scripts do not delete system identities.' },
12
22
  ];
13
23
  if (executor)
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { goBuildEnv } from "../build-spk/dsm-arch.js";
7
- import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_MIN_DSM, portalMinDsmMessage } from "../build-spk/dsm-version.js";
7
+ import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
8
8
  import { checkCgiBinary } from "./binary.js";
9
9
  import { loadStandaloneManifest } from "./manifest.js";
10
10
  import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
@@ -80,7 +80,7 @@ export async function buildStandalone(options) {
80
80
  if (manifest.capabilities.rootOps === true && !isDsmAtLeast(manifest.os_min_ver, DEFAULT_MIN_DSM)) {
81
81
  throw new StandaloneBuildError("STANDALONE_OS_MIN_VER_INVALID", `root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
82
82
  }
83
- if (manifest.portal && !isDsmAtLeast(manifest.os_min_ver, PORTAL_MIN_DSM)) {
83
+ if (manifest.portal && !isDsmAtLeast(manifest.os_min_ver, PORTAL_EXECUTOR_MIN_DSM)) {
84
84
  throw new StandaloneBuildError("STANDALONE_PORTAL_MIN_DSM", `[STANDALONE_PORTAL_MIN_DSM] ${portalMinDsmMessage(manifest.os_min_ver, "portal")}`);
85
85
  }
86
86
  const jobs = checkJobs(options.jobs);
@@ -160,7 +160,7 @@ async function backendFor(manifest, arch, work, run) {
160
160
  ...(manifest.capabilities.identity ? [`-X main.identity=${manifest.capabilities.identity}`] : []),
161
161
  ...(manifest.capabilities.files || manifest.capabilities.delegate ? [`-X main.fileStation=session`] : []),
162
162
  ...(manifest.entry ? [`-X main.entry=${manifest.entry}`] : []),
163
- ...(manifest.portal ? ["-X main.portal=on"] : []),
163
+ ...(manifest.portal ? [`-X main.portal=${portalByExecutor(manifest.os_min_ver) ? "executor" : "on"}`] : []),
164
164
  ].join(" ");
165
165
  const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, backend.main], {
166
166
  cwd: backend.dir,
@@ -4,6 +4,7 @@ import { PASSWORD_CONFIRM, ROOT_PANEL, ROOT_RUNTIME, rootPanelCatalogs } from ".
4
4
  import { HOST_ADAPTER_RUNTIME } from "./host-adapter.js";
5
5
  import { permitsOrdinaryUsers } from "./api-permissions.js";
6
6
  import { PORTAL_RUNTIME } from "./portal-window.js";
7
+ import { portalByExecutor } from "../build-spk/dsm-version.js";
7
8
  import { uiTitleFields } from "../build-spk/ui-texts.js";
8
9
  /** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
9
10
  export const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
@@ -172,6 +173,15 @@ ${root ? root.definitions : ""}${manifest.portal ? renderPortalDefinition(ns) :
172
173
  document.head.appendChild(js);
173
174
  };
174
175
 
176
+ // DSM reopens a window at the size it was last left at, even one saved on a
177
+ // larger screen, and only moves a window that does not fit: keep it inside.
178
+ ${ns}.fitDesktop = function (size) {
179
+ var taskbar = SYNO.SDS.TaskBar ? SYNO.SDS.TaskBar.height() : 0;
180
+ size.width = Math.min(size.width, Ext.lib.Dom.getViewWidth());
181
+ size.height = Math.min(size.height, Ext.lib.Dom.getViewHeight() - taskbar);
182
+ return size;
183
+ };
184
+
175
185
  Ext.define(${JSON.stringify(`${ns}.Instance`)}, {
176
186
  extend: "SYNO.SDS.AppInstance",
177
187
  appWindowName: ${JSON.stringify(`${ns}.MainWindow`)}
@@ -180,10 +190,13 @@ Ext.define(${JSON.stringify(`${ns}.Instance`)}, {
180
190
  Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
181
191
  extend: "SYNO.SDS.AppWindow",
182
192
  constructor: function (config) {
183
- this.callParent([Ext.apply({
193
+ this.callParent([${ns}.fitDesktop(Ext.apply({
184
194
  width: 960, height: 640, minWidth: 700, minHeight: 420, layout: "fit",
185
195
  ${root ? root.html : `html: '<div class="dsmapp-root" style="height:100%"></div>'`}
186
- }, config)]);
196
+ }, config))]);
197
+ },
198
+ getRestoreSizePos: function (config) {
199
+ return ${ns}.fitDesktop(Ext.apply({ width: config.width, height: config.height }, this.callParent(arguments)));
187
200
  },
188
201
  onOpen: function () {
189
202
  this.callParent(arguments);
@@ -267,7 +280,7 @@ ${PORTAL_RUNTIME}
267
280
  function renderPortalAccess(ns, manifest) {
268
281
  return `${ns}.createPortalAccess({
269
282
  appId: ${JSON.stringify(`${ns}.Instance`)},
270
- defaultAlias: ${JSON.stringify(manifest.portal.alias)},
283
+ defaultAlias: ${JSON.stringify(manifest.portal.alias)},${portalByExecutor(manifest.os_min_ver) ? "\n entry: true," : ""}
271
284
  isAdmin: function () { return ${ns}.sessionUser().isAdmin; },
272
285
  send: ${ns}.webApi(me),
273
286
  call: ${ns}.call,