@synopackageland/cli 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +234 -1
  2. package/dist/add.d.ts +4 -1
  3. package/dist/add.js +14 -9
  4. package/dist/build-spk/dsm-platforms.json +4 -1
  5. package/dist/build-spk/gateway.d.ts +1 -1
  6. package/dist/build-spk/gateway.js +8 -4
  7. package/dist/build-spk/go-toolchain.d.ts +20 -0
  8. package/dist/build-spk/go-toolchain.js +44 -0
  9. package/dist/cli.js +13 -7
  10. package/dist/deploy/native-install-values.d.ts +17 -0
  11. package/dist/deploy/native-install-values.js +32 -0
  12. package/dist/deploy/plan.d.ts +6 -2
  13. package/dist/deploy/plan.js +57 -6
  14. package/dist/error-registry.js +12 -6
  15. package/dist/host/dsm-client.js +53 -17
  16. package/dist/host/dsm-http.js +24 -2
  17. package/dist/host/types.d.ts +1 -0
  18. package/dist/info.js +28 -9
  19. package/dist/init.js +1 -1
  20. package/dist/runtime/dsm-version.d.ts +6 -0
  21. package/dist/runtime/dsm-version.js +8 -0
  22. package/dist/runtime/executor-build.d.ts +1 -1
  23. package/dist/runtime/executor-build.js +8 -5
  24. package/dist/standalone/daemon.d.ts +2 -1
  25. package/dist/standalone/daemon.js +10 -6
  26. package/dist/standalone/dsm-window.js +6 -34
  27. package/dist/standalone/inspect-spk.js +5 -1
  28. package/dist/standalone/native-authorization-window.d.ts +1 -1
  29. package/dist/standalone/native-authorization-window.js +7 -1
  30. package/dist/standalone/native-root-window.d.ts +4 -2
  31. package/dist/standalone/native-root-window.js +33 -18
  32. package/dist/standalone/root-window.d.ts +4 -1
  33. package/dist/standalone/root-window.js +143 -33
  34. package/dist/templates/_daemon/go.mod +1 -1
  35. package/dist/templates/_daemon/main.go +6 -3
  36. package/dist/templates/_daemon/main_test.go +21 -1
  37. package/dist/templates/_daemon/testdata/child.go +3 -0
  38. package/dist/templates/_executor/README.md +4 -1
  39. package/dist/templates/_executor/anchor.go +10 -0
  40. package/dist/templates/_executor/anchor_test.go +40 -5
  41. package/dist/templates/_executor/digest_review_test.go +2 -2
  42. package/dist/templates/_executor/dsm.go +40 -3
  43. package/dist/templates/_executor/go.mod +2 -0
  44. package/dist/templates/_executor/main.go +11 -5
  45. package/dist/templates/_executor/managed.go +3 -3
  46. package/dist/templates/_executor/managed_test.go +3 -3
  47. package/dist/templates/_executor/path_test.go +18 -0
  48. package/dist/templates/_executor/pid.go +37 -0
  49. package/dist/templates/_executor/pid_test.go +113 -0
  50. package/dist/templates/_executor/platform_other.go +8 -0
  51. package/dist/templates/_executor/purge_log.go +18 -0
  52. package/dist/templates/_executor/tasks.go +45 -7
  53. package/dist/templates/_executor/tasks_dsm71_test.go +114 -0
  54. package/dist/templates/_executor/tasks_test.go +58 -1
  55. package/dist/templates/_executor/upgrade_watch_test.go +13 -5
  56. package/dist/templates/_gateway/go.mod +2 -0
  57. package/dist/templates/native-go/backend/go.mod +1 -1
  58. package/dist/templates/native-go/backend/internal/filestation/client.go +39 -3
  59. package/dist/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
  60. package/dist/templates/native-go/backend/internal/identity/README.md +5 -1
  61. package/dist/templates/native-go/backend/internal/identity/dsm.go +24 -5
  62. package/dist/templates/native-go/backend/internal/identity/http.go +1 -1
  63. package/dist/templates/native-go/backend/internal/identity/identity_test.go +48 -1
  64. package/dist/templates/native-go/backend/internal/identity/service.go +1 -1
  65. package/dist/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
  66. package/dist/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
  67. package/dist/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
  68. package/dist/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
  69. package/dist/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
  70. package/dist/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
  71. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
  72. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
  73. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
  74. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
  75. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
  76. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
  77. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
  78. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
  79. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
  80. package/package.json +1 -1
  81. package/templates/_daemon/go.mod +1 -1
  82. package/templates/_daemon/main.go +6 -3
  83. package/templates/_daemon/main_test.go +21 -1
  84. package/templates/_daemon/testdata/child.go +3 -0
  85. package/templates/_executor/README.md +4 -1
  86. package/templates/_executor/anchor.go +10 -0
  87. package/templates/_executor/anchor_test.go +40 -5
  88. package/templates/_executor/digest_review_test.go +2 -2
  89. package/templates/_executor/dsm.go +40 -3
  90. package/templates/_executor/go.mod +2 -0
  91. package/templates/_executor/main.go +11 -5
  92. package/templates/_executor/managed.go +3 -3
  93. package/templates/_executor/managed_test.go +3 -3
  94. package/templates/_executor/path_test.go +18 -0
  95. package/templates/_executor/pid.go +37 -0
  96. package/templates/_executor/pid_test.go +113 -0
  97. package/templates/_executor/platform_other.go +8 -0
  98. package/templates/_executor/purge_log.go +18 -0
  99. package/templates/_executor/tasks.go +45 -7
  100. package/templates/_executor/tasks_dsm71_test.go +114 -0
  101. package/templates/_executor/tasks_test.go +58 -1
  102. package/templates/_executor/upgrade_watch_test.go +13 -5
  103. package/templates/_gateway/go.mod +2 -0
  104. package/templates/native-go/backend/go.mod +1 -1
  105. package/templates/native-go/backend/internal/filestation/client.go +39 -3
  106. package/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
  107. package/templates/native-go/backend/internal/identity/README.md +5 -1
  108. package/templates/native-go/backend/internal/identity/dsm.go +24 -5
  109. package/templates/native-go/backend/internal/identity/http.go +1 -1
  110. package/templates/native-go/backend/internal/identity/identity_test.go +48 -1
  111. package/templates/native-go/backend/internal/identity/service.go +1 -1
  112. package/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
  113. package/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
  114. package/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
  115. package/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
  116. package/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
  117. package/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
  118. package/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
  119. package/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
  120. package/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
  121. package/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
  122. package/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
  123. package/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
  124. package/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
  125. package/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
  126. package/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
@@ -3,14 +3,18 @@ import { loadContract } from "../contract/load.js";
3
3
  import { validateProject } from "../validate.js";
4
4
  import { findStandaloneRoot, loadStandaloneManifest, } from "../standalone/manifest.js";
5
5
  import { buildStandalone } from "../standalone/build.js";
6
+ import { buildNativeLayout, isNativeLayout, loadNativeLayout } from "../standalone/native-layout.js";
6
7
  import { resolveDeployIdentity } from "./identity.js";
7
8
  import { nextDeployVersion } from "./spk-version.js";
9
+ import { resolveNativeInstallValues } from "./native-install-values.js";
8
10
  function fail(code, message) {
9
11
  return Object.assign(new Error(`[${code}] ${message}`), { code });
10
12
  }
11
13
  /** Purely local planning: no credentials, network, build or installation. */
12
14
  export function planDeployment(root, options = {}) {
13
15
  const standalone = findStandaloneRoot(root);
16
+ if (standalone && isNativeLayout(standalone))
17
+ return planNativeLayout(standalone, options);
14
18
  if (standalone) {
15
19
  const validation = validateProject(findProjectRoot(standalone));
16
20
  if (!validation.ok)
@@ -33,7 +37,7 @@ export function planDeployment(root, options = {}) {
33
37
  transport: "dsm-local-spk",
34
38
  notes: m.capabilities.rootOps || m.capabilities.delegate
35
39
  ? [
36
- "Installation does not mean ready. Approve boot and control tasks once in the App window; executor or policy digest changes require approval again.",
40
+ "Installation does not mean ready. Approve boot and control tasks once in the App window. An executor-only upgrade stays ready without approval (INFO anchors the executor digest); a policy digest change requires approval again.",
37
41
  ]
38
42
  : [],
39
43
  };
@@ -59,7 +63,56 @@ export function planDeployment(root, options = {}) {
59
63
  ],
60
64
  };
61
65
  }
62
- export async function deployStandalone(plan, host) {
66
+ /** DSM reports the aarch64 family; a native layout may name it armv8. */
67
+ function layoutArchFor(arches, family) {
68
+ const names = family === "aarch64" ? ["aarch64", "armv8"] : [family];
69
+ return names.find((name) => arches.includes(name));
70
+ }
71
+ function loadLayout(root) {
72
+ const loaded = loadNativeLayout(root);
73
+ if (!loaded.ok)
74
+ throw fail(loaded.errors[0].code, loaded.errors.map((e) => e.message).join("\n"));
75
+ return loaded.manifest;
76
+ }
77
+ function planNativeLayout(root, options) {
78
+ const m = loadLayout(root);
79
+ if (options.dev)
80
+ throw fail("DEVELOPMENT_INSTANCE_UNSUPPORTED", "--dev is not supported for a native layout; choose a distinct package name");
81
+ if (options.arch && !layoutArchFor(m.arches, options.arch))
82
+ throw fail("STANDALONE_ARCH_NOT_DECLARED", `Architecture not declared: ${options.arch}; add it to dsm-package.yaml arches to deploy it`);
83
+ return {
84
+ root,
85
+ kind: "native-layout",
86
+ packageId: m.package,
87
+ version: `${m.version}-${String(m.build).padStart(4, "0")}`,
88
+ arch: options.arch,
89
+ capabilities: [],
90
+ transport: "dsm-local-spk",
91
+ notes: [],
92
+ };
93
+ }
94
+ async function buildForDeploy(plan, arch, installed, options) {
95
+ if (plan.kind === "native-layout") {
96
+ const m = loadLayout(plan.root);
97
+ const layoutArch = layoutArchFor(m.arches, arch);
98
+ if (!layoutArch)
99
+ throw fail("STANDALONE_ARCH_NOT_DECLARED", `Architecture not declared: ${arch}; add it to dsm-package.yaml arches to deploy it`);
100
+ const name = Object.keys(options.params ?? {})[0];
101
+ if (name)
102
+ throw fail("DEPLOY_PARAM_UNDECLARED", `${m.package} has no install wizard values; remove --param ${name}.`);
103
+ const { outputs } = await buildNativeLayout({ root: plan.root, arches: [layoutArch], run: options.run });
104
+ return { outputs, extraValues: {} };
105
+ }
106
+ const loaded = loadStandaloneManifest(plan.root);
107
+ if (!loaded.ok)
108
+ throw fail(loaded.errors[0].code, loaded.errors.map((e) => e.message).join("\n"));
109
+ const values = resolveNativeInstallValues(loaded.manifest, options.params ?? {}, { installed });
110
+ if (!values.ok)
111
+ throw fail(values.code, values.message);
112
+ const { outputs } = await buildStandalone({ root: plan.root, arches: [arch], run: options.run });
113
+ return { outputs, extraValues: values.extraValues };
114
+ }
115
+ export async function deployStandalone(plan, host, options = {}) {
63
116
  if (plan.kind === "container")
64
117
  throw fail("DEPLOY_PLAN_INVALID", "Expected native-go or binary deployment plan");
65
118
  const arch = plan.arch ?? (await host.getArchFamily?.());
@@ -70,16 +123,14 @@ export async function deployStandalone(plan, host) {
70
123
  nextDeployVersion(plan.version, installed) !== plan.version) {
71
124
  throw fail("DEPLOY_VERSION_INVALID", `Increase dsm-package.yaml version/build above installed ${installed}`);
72
125
  }
73
- const { outputs } = await buildStandalone({
74
- root: plan.root,
75
- arches: [arch],
76
- });
126
+ const { outputs, extraValues } = await buildForDeploy(plan, arch, Boolean(installed), options);
77
127
  if (outputs.length !== 1)
78
128
  throw fail("DEPLOY_BUILD_OUTPUT_INVALID", "Expected one SPK");
79
129
  await host.installFromLocalSpk({
80
130
  packageId: plan.packageId,
81
131
  version: plan.version,
82
132
  spkPath: outputs[0],
133
+ extraValues,
83
134
  });
84
135
  return outputs[0];
85
136
  }
@@ -97,12 +97,12 @@ export const ERROR_REGISTRY = {
97
97
  "GATEWAY_GO_MISSING": {
98
98
  "code": "GATEWAY_GO_MISSING",
99
99
  "description": "建置 DSM 驗證閘道需要 Go,但找不到 go 指令。",
100
- "suggestion": "請安裝 Go 1.24 以上版本(https://go.dev/dl/),或在 integration.web.access 明確選 public。"
100
+ "suggestion": "請安裝 Go 1.21 以上版本(https://go.dev/dl/,建置時固定使用 go1.26.9),或在 integration.web.access 明確選 public。"
101
101
  },
102
102
  "GATEWAY_BUILD_FAILED": {
103
103
  "code": "GATEWAY_BUILD_FAILED",
104
104
  "description": "DSM 驗證閘道建置失敗或產出不是對應架構的靜態 ELF。",
105
- "suggestion": "請查看 go build 輸出;確認 Go 版本 1.24 以上且 templates/_gateway 未被修改。"
105
+ "suggestion": "請查看 go build 輸出;閘道固定以 go1.26.9 建置,離線時請安裝 go1.26.9 或讓 GOPROXY 可提供 golang.org/toolchain,並確認 templates/_gateway 未被修改。"
106
106
  },
107
107
  "CONTRACT_INVALID_STRING": {
108
108
  "code": "CONTRACT_INVALID_STRING",
@@ -346,8 +346,13 @@ export const ERROR_REGISTRY = {
346
346
  },
347
347
  "DEPLOY_PARAM_UNDECLARED": {
348
348
  "code": "DEPLOY_PARAM_UNDECLARED",
349
- "description": "deploy --param 提供了契約未宣告的 Install Parameter。",
350
- "suggestion": "請只傳 synology-app.yaml install 中宣告的 string 或 boolean 名稱。"
349
+ "description": "deploy --param 提供了契約未宣告的 Install Parameter,或 native-go/binary 套件沒有的安裝精靈值。",
350
+ "suggestion": "Compose 請只傳 synology-app.yaml install 中宣告的 string 或 boolean 名稱;native-go/binary 只接受 install-selected delegate 的 delegate_share 或 delegate_home。"
351
+ },
352
+ "DEPLOY_PARAM_REQUIRED": {
353
+ "code": "DEPLOY_PARAM_REQUIRED",
354
+ "description": "首次安裝 install-selected delegate 套件時沒有提供安裝精靈的範圍值,preinst 會拒絕安裝。",
355
+ "suggestion": "加上 --param delegate_share=<共用資料夾> 或 --param delegate_home=true 再部署。"
351
356
  },
352
357
  "DEPLOY_PARAM_TYPE_INVALID": {
353
358
  "code": "DEPLOY_PARAM_TYPE_INVALID",
@@ -412,7 +417,7 @@ export const ERROR_REGISTRY = {
412
417
  "STANDALONE_DAEMON_BUILD_FAILED": {
413
418
  "code": "STANDALONE_DAEMON_BUILD_FAILED",
414
419
  "description": "無法建置套件 daemon 的 Go supervisor。",
415
- "suggestion": "安裝 Go 1.24 以上並檢查 compiler stderr;NAS 不需要 Go toolchain。"
420
+ "suggestion": "安裝 Go 1.21 以上(建置固定使用 go1.26.9,離線時請先備妥)並檢查 compiler stderr;NAS 不需要 Go toolchain。"
416
421
  },
417
422
  "STANDALONE_DAEMON_PAYLOAD_INVALID": {
418
423
  "code": "STANDALONE_DAEMON_PAYLOAD_INVALID",
@@ -685,7 +690,7 @@ export function getErrorCode(code) {
685
690
  return ERROR_REGISTRY[code];
686
691
  }
687
692
  for (const [code, description, suggestion] of [
688
- ["EXECUTOR_BUILD_FAILED", "executor 或 lifecycle helper 編譯失敗,或 CLI 套件缺少 executor 原始碼。", "確認已安裝 Go,查看訊息中的編譯器輸出;原始碼缺少時重新安裝 CLI。"],
693
+ ["EXECUTOR_BUILD_FAILED", "executor 或 lifecycle helper 編譯失敗,或 CLI 套件缺少 executor 原始碼。", "確認已安裝 Go 1.21 以上且可取得 go1.26.9(離線時請先備妥),查看訊息中的編譯器輸出;原始碼缺少時重新安裝 CLI。"],
689
694
  ["EXECUTOR_PIN_REQUIRED", "宣告 rootOps 或 delegate 的套件建置時沒有 executor 產物。", "使用 synopkgland build,讓 CLI 編譯並 pin executor;不要直接呼叫低階 SPK API。"],
690
695
  ["EXECUTOR_POLICY", "capabilities.delegate 的 operations 或 scope 不合法,無法產生 executor policy。", "改用 init/add 產生的 delegate 宣告,或依 manifest 參考修正 scope 與 operations。"],
691
696
  ["EXECUTOR_PACKAGE", "pin 進 root task 的套件名稱不合法。", "package 必須以英文字母開頭,只含英數字(2–64 字)。"],
@@ -697,3 +702,4 @@ for (const [code, description, suggestion] of [
697
702
  ERROR_REGISTRY["STANDALONE_ENTRY_INVALID"] = { "code": "STANDALONE_ENTRY_INVALID", "description": "entry 設定不合法。", "suggestion": "entry 填 admin-only 或 app-privilege;app-privilege 需要至少一個 authenticated route。" };
698
703
  ERROR_REGISTRY["STANDALONE_ENTRY_WIDENED"] = { "code": "STANDALONE_ENTRY_WIDENED", "description": "entry: admin-only 與放寬權限的 route 設定衝突。", "suggestion": "把 permissions.json 中 authenticated 或 public 的 route 改為 administrators;要開放給一般使用者請改用 entry: app-privilege 或省略 entry。" };
699
704
  ERROR_REGISTRY["STANDALONE_ENTRY_UNSUPPORTED"] = { "code": "STANDALONE_ENTRY_UNSUPPORTED", "description": "backend 無法強制 entry 模式。", "suggestion": "使用 Go backend,複製 SDK 的 internal/dsm entry gate 並在 cmd/app/main.go 呼叫 dsm.NewEntryGate。" };
705
+ ERROR_REGISTRY["DSM_TLS_UNTRUSTED"] = { "code": "DSM_TLS_UNTRUSTED", "description": "DSM 的 TLS 憑證不受這台電腦信任(自簽、過期或名稱不符)。", "suggestion": "把 NAS 憑證或其 CA 匯出成 PEM,用 NODE_EXTRA_CA_CERTS=<該檔> 執行;或讓 DSM 使用與 host profile 名稱相符的受信任憑證。不要關閉憑證驗證。" };
@@ -3,6 +3,7 @@ import { listShareNamesWithSynoagentcli, lookupSharePathWithSynoagentcli, } from
3
3
  import { createSynoagentcliApiCall, fetchSharePermissions, fetchUserGroups, } from "./share-permission-lookup.js";
4
4
  import { fetchPortInfo } from "./port-info-lookup.js";
5
5
  import { dsmFetch } from "./dsm-http.js";
6
+ import { versionUnsupported } from "../runtime/dsm-version.js";
6
7
  import { fetchDsmPortProfileInventory } from "./dsm-port-profile.js";
7
8
  import { installPackageFromLocalSpk, fetchInstalledPackageState, packageStateFromRecord, } from "./catalog-install.js";
8
9
  import { resolveHostCredentials } from "./credentials.js";
@@ -53,6 +54,16 @@ export function homesPathFromDsmUserHomeRead(result) {
53
54
  }
54
55
  return `${volume}/homes`;
55
56
  }
57
+ function setCookiePairs(headers) {
58
+ const lines = typeof headers.getSetCookie === "function"
59
+ ? headers.getSetCookie()
60
+ : headers.get("set-cookie")
61
+ ? [headers.get("set-cookie")]
62
+ : [];
63
+ return lines
64
+ .map((line) => line.split(";")[0]?.trim())
65
+ .filter((pair) => Boolean(pair));
66
+ }
56
67
  function createDsmNasHost(credentials, profileName) {
57
68
  const dsmOrigin = credentials.dsmOrigin.replace(/\/$/, "");
58
69
  let cookieHeader = credentials.cookieHeader;
@@ -77,15 +88,7 @@ function createDsmNasHost(credentials, profileName) {
77
88
  const login = (await response.json());
78
89
  if (!response.ok || login.success !== true)
79
90
  throw new Error("DSM login failed");
80
- const setCookie = typeof response.headers.getSetCookie === "function"
81
- ? response.headers.getSetCookie()
82
- : response.headers.get("set-cookie")
83
- ? [response.headers.get("set-cookie")]
84
- : [];
85
- const pairs = setCookie
86
- .map((line) => line.split(";")[0]?.trim())
87
- .filter((pair) => Boolean(pair));
88
- cookieHeader = pairs.join("; ");
91
+ cookieHeader = setCookiePairs(response.headers).join("; ");
89
92
  if (!cookieHeader)
90
93
  throw new Error("DSM login returned no session cookie");
91
94
  };
@@ -106,12 +109,38 @@ function createDsmNasHost(credentials, profileName) {
106
109
  async getExecutorStatus(packageIdentity) {
107
110
  try {
108
111
  await ensureLogin();
109
- const response = await dsmFetch(`${dsmOrigin}/webman/3rdparty/${encodeURIComponent(packageIdentity)}/cgi/api.cgi`, {
110
- method: "POST", headers: { cookie: cookieHeader ?? "", "content-type": "application/json" },
111
- body: JSON.stringify({ method: "executor.status", params: {} }),
112
- });
113
- const body = await response.json();
114
- return body.success ? body.data : undefined;
112
+ const appCall = async (method, cookie) => {
113
+ const response = await dsmFetch(`${dsmOrigin}/webman/3rdparty/${encodeURIComponent(packageIdentity)}/cgi/api.cgi`, {
114
+ method: "POST", headers: { cookie, origin: dsmOrigin, "content-type": "application/json" },
115
+ body: JSON.stringify({ method, params: method === "identity.session" ? { bootstrap: true } : {} }),
116
+ });
117
+ const body = await response.json();
118
+ return { body, setCookie: setCookiePairs(response.headers) };
119
+ };
120
+ // A paused executor answers with why and what to do next.
121
+ const explain = (status) => status.state === "paused" ? { ...status, error: `Executor paused (${status.reason}). ${status.next}` } : status;
122
+ const direct = await appCall("executor.status", cookieHeader ?? "");
123
+ if (direct.body.success)
124
+ return explain(direct.body.data);
125
+ // identity: standalone wraps business methods in an App session; trade the
126
+ // administrator's DSM cookie for one, read once, then end that session.
127
+ if (direct.body.error?.code !== "session_required")
128
+ return undefined;
129
+ const session = await appCall("identity.session", cookieHeader ?? "");
130
+ if (!session.body.success) {
131
+ const code = session.body.error?.code;
132
+ return { ready: false, executor_digest: "", policy_digest: "", error: code === "https_required"
133
+ ? "identity: standalone status needs an https:// DSM host profile to open an App session."
134
+ : `identity: standalone App session bootstrap failed (${code ?? "error"}).` };
135
+ }
136
+ const appCookie = [cookieHeader ?? "", ...session.setCookie].filter(Boolean).join("; ");
137
+ try {
138
+ const status = await appCall("executor.status", appCookie);
139
+ return status.body.success ? explain(status.body.data) : undefined;
140
+ }
141
+ finally {
142
+ await appCall("identity.logout", appCookie).catch(() => undefined);
143
+ }
115
144
  }
116
145
  catch {
117
146
  return undefined;
@@ -130,6 +159,12 @@ function createDsmNasHost(credentials, profileName) {
130
159
  throw Object.assign(new Error(`DSM ${body.error?.code ?? "error"}: scheduler readback denied; check the administrator session. No retry was made.`), { code: body.error?.code });
131
160
  return body.data;
132
161
  };
162
+ // DSM 7.1 stops at TaskScheduler list v2 (offset) and get v3; only an unsupported version falls back.
163
+ const taskRead = (method, version, params, legacyVersion, legacyParams) => call("SYNO.Core.TaskScheduler", method, version, params).catch((e) => {
164
+ if (!versionUnsupported(e.code))
165
+ throw e;
166
+ return call("SYNO.Core.TaskScheduler", method, legacyVersion, legacyParams);
167
+ });
133
168
  const records = await call("SYNO.Core.EventScheduler", "list", 1, {});
134
169
  if (!Array.isArray(records))
135
170
  throw new Error("Invalid EventScheduler readback.");
@@ -142,13 +177,14 @@ function createDsmNasHost(credentials, profileName) {
142
177
  throw new Error("Invalid boot task readback.");
143
178
  tasks.push({ ...detail, name: detail.task_name, kind: "boot" });
144
179
  }
145
- const listed = await call("SYNO.Core.TaskScheduler", "list", 3, { start: 0, limit: 500 });
180
+ const listed = await taskRead("list", 3, { start: 0, limit: 500 }, 2, { offset: 0, limit: 500 });
146
181
  if (!Array.isArray(listed?.tasks) || !Number.isInteger(listed.total) || listed.total !== listed.tasks.length || listed.total > 500)
147
182
  throw new Error("Incomplete TaskScheduler readback.");
148
183
  for (const summary of listed.tasks) {
149
184
  if (summary?.name !== `${packageIdentity} executor control`)
150
185
  continue;
151
- const detail = await call("SYNO.Core.TaskScheduler", "get", 4, { id: summary.id, real_owner: summary.real_owner });
186
+ const ref = { id: summary.id, real_owner: summary.real_owner };
187
+ const detail = await taskRead("get", 4, ref, 3, ref);
152
188
  if (!detail || String(detail.id) !== String(summary.id) || detail.name !== summary.name)
153
189
  throw new Error("Invalid control task readback.");
154
190
  tasks.push({ ...detail, kind: "control" });
@@ -2,11 +2,29 @@ import { spawnSync } from "node:child_process";
2
2
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- function isHostUnreachable(error) {
6
- const code = error.cause?.code
5
+ function errorCode(error) {
6
+ return error.cause?.code
7
7
  ?? error.code;
8
+ }
9
+ function isHostUnreachable(error) {
10
+ const code = errorCode(error);
8
11
  return code === "EHOSTUNREACH" || code === "ENETUNREACH";
9
12
  }
13
+ const UNTRUSTED_CERTIFICATE = new Set([
14
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
15
+ "SELF_SIGNED_CERT_IN_CHAIN",
16
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
17
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
18
+ "CERT_HAS_EXPIRED",
19
+ "ERR_TLS_CERT_ALTNAME_INVALID",
20
+ ]);
21
+ /** Node reports an untrusted certificate only as "fetch failed"; say what to do. Verification stays on. */
22
+ function untrustedCertificate(url, code) {
23
+ const origin = new URL(url).origin;
24
+ return Object.assign(new Error(`DSM at ${origin} presented a TLS certificate this computer does not trust (${code}). ` +
25
+ `Export the NAS certificate or its CA as PEM and run with NODE_EXTRA_CA_CERTS=<that file>, ` +
26
+ `or give DSM a certificate whose name matches the host profile. Certificate checks stay on.`), { code: "DSM_TLS_UNTRUSTED" });
27
+ }
10
28
  function headerRecord(headers) {
11
29
  if (!headers) {
12
30
  return {};
@@ -24,6 +42,10 @@ export async function dsmFetch(url, init = {}) {
24
42
  return await fetch(url, init);
25
43
  }
26
44
  catch (error) {
45
+ const code = errorCode(error);
46
+ if (code && UNTRUSTED_CERTIFICATE.has(code)) {
47
+ throw untrustedCertificate(url, code);
48
+ }
27
49
  if (!isHostUnreachable(error)) {
28
50
  throw error;
29
51
  }
@@ -33,6 +33,7 @@ export interface NasHost {
33
33
  ready: boolean;
34
34
  executor_digest: string;
35
35
  policy_digest: string;
36
+ error?: string;
36
37
  } | undefined>;
37
38
  getArchFamily?(): Promise<NativeArchFamily | undefined>;
38
39
  /** List the current logical Shared Folder names using an administrator session. */
package/dist/info.js CHANGED
@@ -5,6 +5,7 @@ import { appOriginFromContract } from "./host/open-url.js";
5
5
  import { resolveDeployIdentity } from "./deploy/identity.js";
6
6
  import { loadStandaloneManifest, } from "./standalone/manifest.js";
7
7
  import { inspectSpk } from "./standalone/inspect-spk.js";
8
+ import { isNativeLayout, loadNativeLayout } from "./standalone/native-layout.js";
8
9
  import { executorPolicy, executorPolicyText, sha256 } from "./runtime/executor-policy.js";
9
10
  import { executorTasks } from "./runtime/executor-tasks.js";
10
11
  import { uninstallCleanup } from "./runtime/uninstall-cleanup.js";
@@ -72,7 +73,7 @@ async function liveRootCapability(m, host) {
72
73
  return classified;
73
74
  const observed = await host.getExecutorStatus?.(m.package);
74
75
  if (observed?.ready !== true || observed.policy_digest !== policyDigest)
75
- return { ...classified, state: "blocked", reason: "Executor health or policy digest readback could not be verified." };
76
+ return { ...classified, state: "blocked", reason: observed?.error ?? "Executor health or policy digest readback could not be verified." };
76
77
  return { ...classified, reason: "Root tasks and the running executor match the source policy (no local SPK, so executor bytes were not compared)." };
77
78
  }
78
79
  catch {
@@ -116,7 +117,7 @@ async function rootCapability(m, host, version) {
116
117
  if (classified.state === "ready") {
117
118
  const observed = await host.getExecutorStatus?.(m.package);
118
119
  if (!observed || observed.ready !== true || observed.executor_digest !== executor.executorDigest || observed.policy_digest !== executor.policyDigest)
119
- return { ...classified, executor, state: "blocked", reason: "Executor health or digest readback could not be verified." };
120
+ return { ...classified, executor, state: "blocked", reason: observed?.error ?? "Executor health or digest readback could not be verified." };
120
121
  }
121
122
  return { ...classified, executor };
122
123
  }
@@ -127,12 +128,14 @@ async function rootCapability(m, host, version) {
127
128
  export async function standaloneInfo(options) {
128
129
  if (options.dev)
129
130
  throw Object.assign(new Error("--dev is not supported for native-go/binary; choose a distinct package name at init"), { code: "DEVELOPMENT_INSTANCE_UNSUPPORTED" });
131
+ if (isNativeLayout(options.root))
132
+ return nativeLayoutInfo(options.root, options.host);
130
133
  const loaded = loadStandaloneManifest(options.root);
131
134
  if (!loaded.ok)
132
135
  throw Object.assign(new Error(loaded.errors.map((e) => e.message).join("\n")), { code: loaded.errors[0].code });
133
136
  const m = loaded.manifest;
134
- const remote = await options.host.getInstalledPackageState?.(m.package);
135
- const installed = await options.host.isPackageInstalled(m.package);
137
+ const { remote, ...state } = await packageState(m.package, options.host);
138
+ const installed = state.installed;
136
139
  const capabilities = [];
137
140
  let executor;
138
141
  for (const name of ["files", "jobs", "identity", "rootOps", "delegate"]) {
@@ -158,16 +161,32 @@ export async function standaloneInfo(options) {
158
161
  });
159
162
  }
160
163
  return {
161
- packageIdentity: m.package,
162
- developmentInstance: false,
163
- installed,
164
- readiness: remote?.status ?? (installed ? "UNKNOWN" : "NOT_INSTALLED"),
165
- openUrl: `${options.host.dsmOrigin.replace(/\/$/, "")}/?launchApp=SYNO.SDS.${m.package}.Instance`,
164
+ ...state,
166
165
  capabilities,
167
166
  ...(executor ? { executor } : {}),
168
167
  uninstallCleanup: uninstallCleanup(m.package, Boolean(m.capabilities.rootOps || m.capabilities.delegate)),
169
168
  };
170
169
  }
170
+ /** A native layout has no SDK capabilities; report package state only. */
171
+ async function nativeLayoutInfo(root, host) {
172
+ const loaded = loadNativeLayout(root);
173
+ if (!loaded.ok)
174
+ throw Object.assign(new Error(loaded.errors.map((e) => e.message).join("\n")), { code: loaded.errors[0].code });
175
+ const { remote, ...state } = await packageState(loaded.manifest.package, host);
176
+ return { ...state, capabilities: [] };
177
+ }
178
+ async function packageState(pkg, host) {
179
+ const remote = await host.getInstalledPackageState?.(pkg);
180
+ const installed = await host.isPackageInstalled(pkg);
181
+ return {
182
+ remote,
183
+ packageIdentity: pkg,
184
+ developmentInstance: false,
185
+ installed,
186
+ readiness: remote?.status ?? (installed ? "UNKNOWN" : "NOT_INSTALLED"),
187
+ openUrl: `${host.dsmOrigin.replace(/\/$/, "")}/?launchApp=SYNO.SDS.${pkg}.Instance`,
188
+ };
189
+ }
171
190
  export function formatProjectInfo(info) {
172
191
  return [
173
192
  `Package Identity:${info.packageIdentity}`,
package/dist/init.js CHANGED
@@ -195,7 +195,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
195
195
  }
196
196
  if (executor) {
197
197
  const path = join(targetDir, "backend/internal/app/routes_test.go");
198
- writeFileSync(path, readFileSync(path, "utf8").replace('if strings.HasPrefix(r.Method(), "jobs.") {', 'if r.Method() == "executor.status" { want = "internal" }\n\t\tif r.Method() == "root.run" { want = "session_required" }\n\t\tif strings.HasPrefix(r.Method(), "jobs.") {'));
198
+ writeFileSync(path, readFileSync(path, "utf8").replace('if strings.HasPrefix(r.Method(), "jobs.") {', 'if r.Method() == "root.run" { want = "session_required" }\n\t\tif strings.HasPrefix(r.Method(), "jobs.") {'));
199
199
  }
200
200
  if (background && ui === "vue") {
201
201
  const app = join(targetDir, "frontend/src/App.vue");
@@ -0,0 +1,6 @@
1
+ /** Self-contained: the same function is embedded in the DSM window. True only
2
+ * when DSM refused a request because this method version is unavailable, the
3
+ * one refusal that may be retried with an older version. DSM 7.1.1 (t-0234)
4
+ * returns 104 for a version above maxVersion and 103 for a method absent at an
5
+ * existing version. Accepts the numeric code and the window's dsm_ form. */
6
+ export declare function versionUnsupported(code: unknown): boolean;
@@ -0,0 +1,8 @@
1
+ /** Self-contained: the same function is embedded in the DSM window. True only
2
+ * when DSM refused a request because this method version is unavailable, the
3
+ * one refusal that may be retried with an older version. DSM 7.1.1 (t-0234)
4
+ * returns 104 for a version above maxVersion and 103 for a method absent at an
5
+ * existing version. Accepts the numeric code and the window's dsm_ form. */
6
+ export function versionUnsupported(code) {
7
+ return ["103", "104", "dsm_103", "dsm_104"].indexOf(String(code)) >= 0;
8
+ }
@@ -1,4 +1,4 @@
1
- import { type NativeArchFamily } from '../build-spk/dsm-arch.js';
1
+ import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
2
2
  import type { CommandRunner } from '../standalone/build.js';
3
3
  import type { StandaloneManifest } from '../standalone/manifest.js';
4
4
  import { type ExecutorPolicy } from './executor-policy.js';
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { templatesRoot } from '../template-paths.js';
4
- import { goBuildEnv } from '../build-spk/dsm-arch.js';
4
+ import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from '../build-spk/go-toolchain.js';
5
5
  import { StandaloneBuildError } from '../standalone/spk.js';
6
6
  import { checkCgiBinary } from '../standalone/binary.js';
7
7
  import { executorPolicy, executorPolicyText, sha256 } from './executor-policy.js';
@@ -15,19 +15,22 @@ export function buildExecutor(m, arch, work, run) {
15
15
  if (!existsSync(join(source, 'go.mod')))
16
16
  throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', 'Executor sources are missing.');
17
17
  const binary = join(dir, 'executor');
18
- const result = run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', binary, '.'], { cwd: source, env: { ...process.env, ...goBuildEnv(arch) } });
18
+ const result = run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', binary, '.'], { cwd: source, env: { ...process.env, ...pinnedGoBuildEnv(arch) } });
19
19
  if (result.missing || result.status !== 0)
20
- throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Executor build failed (${arch}). ${result.stderr}`);
20
+ throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Executor build failed (${arch}). ${PINNED_GO_HINT}\n${result.stderr}`);
21
21
  const problem = checkCgiBinary(work, binary, arch);
22
22
  if (problem)
23
23
  throw new StandaloneBuildError(problem.code, problem.message);
24
24
  const lifecycleBinary = join(dir, 'lifecycle');
25
- const lifecycleResult = run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', lifecycleBinary, './cmd/lifecycle'], { cwd: source, env: { ...process.env, ...goBuildEnv(arch) } });
25
+ const lifecycleResult = run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', lifecycleBinary, './cmd/lifecycle'], { cwd: source, env: { ...process.env, ...pinnedGoBuildEnv(arch) } });
26
26
  if (lifecycleResult.missing || lifecycleResult.status !== 0)
27
- throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Lifecycle helper build failed (${arch}). ${lifecycleResult.stderr}`);
27
+ throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Lifecycle helper build failed (${arch}). ${PINNED_GO_HINT}\n${lifecycleResult.stderr}`);
28
28
  const lifecycleProblem = checkCgiBinary(work, lifecycleBinary, arch);
29
29
  if (lifecycleProblem)
30
30
  throw new StandaloneBuildError(lifecycleProblem.code, lifecycleProblem.message);
31
+ const toolchainProblem = pinnedGoProblem(binary) ?? pinnedGoProblem(lifecycleBinary);
32
+ if (toolchainProblem)
33
+ throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', toolchainProblem);
31
34
  const policy = executorPolicy(m), policyText = executorPolicyText(policy);
32
35
  const pins = { package: m.package, executorDigest: sha256(readFileSync(binary)), policyDigest: sha256(policyText) };
33
36
  return { ...pins, binary, lifecycleBinary, policy, policyText, tasks: executorTasks(pins), implementation: 'executor' };
@@ -1,4 +1,4 @@
1
- import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
1
+ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
2
  import type { CommandRunner } from "./build.js";
3
3
  import type { StandaloneError, StandaloneManifest } from "./manifest.js";
4
4
  export interface Daemon {
@@ -15,6 +15,7 @@ export interface Daemon {
15
15
  start: number;
16
16
  stop: number;
17
17
  status: number;
18
+ uninstall: number;
18
19
  };
19
20
  }
20
21
  export declare function readDaemon(value: unknown, root: string, arches: NativeArchFamily[], fail: (error: StandaloneError) => void): Daemon | undefined;
@@ -1,6 +1,6 @@
1
1
  import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
2
2
  import { isAbsolute, join, relative, resolve } from "node:path";
3
- import { goBuildEnv } from "../build-spk/dsm-arch.js";
3
+ import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from "../build-spk/go-toolchain.js";
4
4
  import { templatesRoot } from "../template-paths.js";
5
5
  import { checkCgiBinary } from "./binary.js";
6
6
  import { StandaloneBuildError } from "./spk.js";
@@ -41,11 +41,12 @@ export function readDaemon(value, root, arches, fail) {
41
41
  if (!record(env) || Object.keys(env).length > 64 || Object.entries(env).some(([k, v]) => !envKey(k) || !token(v)))
42
42
  return invalid();
43
43
  const limits = value.timeouts ?? {};
44
- if (!record(limits) || Object.keys(limits).some(k => !["start", "stop", "status"].includes(k)))
44
+ if (!record(limits) || Object.keys(limits).some(k => !["start", "stop", "status", "uninstall"].includes(k)))
45
45
  return invalid();
46
- const timeouts = { start: limits.start ?? 5, stop: limits.stop ?? 10, status: limits.status ?? 2 };
46
+ // Defaults hold under IO load on a busy NAS; DSM's package unit allows 3600 s.
47
+ const timeouts = { start: limits.start ?? 30, stop: limits.stop ?? 30, status: limits.status ?? 5, uninstall: limits.uninstall ?? 60 };
47
48
  for (const [key, v] of Object.entries(timeouts)) {
48
- if (!Number.isInteger(v) || v < (key === "start" ? 2 : key === "stop" ? 2 : 1) || v > (key === "status" ? 10 : 120))
49
+ if (!Number.isInteger(v) || v < (key === "status" ? 1 : 2) || v > (key === "status" ? 10 : 120))
49
50
  return invalid();
50
51
  }
51
52
  return { exec: value.exec, targets, args, ...(uninstall ? { uninstall_args: uninstall } : {}), env: Object.fromEntries(Object.keys(env).sort().map(key => [key, env[key]])), timeouts: timeouts };
@@ -59,13 +60,16 @@ export function buildDaemonControl(manifest, arch, work, run) {
59
60
  return undefined;
60
61
  const out = join(work, `daemon-control-${arch}`);
61
62
  const result = run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", "-s -w -buildid=", "-o", out, "."], {
62
- cwd: join(templatesRoot(), "_daemon"), env: { ...process.env, ...goBuildEnv(arch) },
63
+ cwd: join(templatesRoot(), "_daemon"), env: { ...process.env, ...pinnedGoBuildEnv(arch) },
63
64
  });
64
65
  if (result.missing || result.status !== 0)
65
- throw new StandaloneBuildError("STANDALONE_DAEMON_BUILD_FAILED", `daemon controller build 需要 Go 1.24 以上。\n${result.stderr}`);
66
+ throw new StandaloneBuildError("STANDALONE_DAEMON_BUILD_FAILED", `daemon controller build failed (${arch}). ${PINNED_GO_HINT}\n${result.stderr}`);
66
67
  const problem = checkCgiBinary(work, out, arch);
67
68
  if (problem)
68
69
  throw new StandaloneBuildError(problem.code, problem.message);
70
+ const toolchainProblem = pinnedGoProblem(out);
71
+ if (toolchainProblem)
72
+ throw new StandaloneBuildError("STANDALONE_DAEMON_BUILD_FAILED", toolchainProblem);
69
73
  return out;
70
74
  }
71
75
  export function copyDaemonPayload(manifest, arch, target, controller) {
@@ -1,6 +1,6 @@
1
1
  import { REOPEN_EVENTS_RUNTIME } from "./reopen-events.js";
2
2
  import { DSM_LANGS } from "./i18n.js";
3
- import { ROOT_PANEL, ROOT_RUNTIME, rootTasks } from "./root-window.js";
3
+ import { PASSWORD_CONFIRM, ROOT_PANEL, ROOT_RUNTIME, rootTasks } from "./root-window.js";
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";
@@ -316,6 +316,10 @@ ${ns}.mountRootPanel = (function () {
316
316
  ${ROOT_PANEL}
317
317
  return mountRootPanel;
318
318
  })();
319
+ ${ns}.createPasswordConfirm = (function () {
320
+ ${PASSWORD_CONFIRM}
321
+ return createPasswordConfirm;
322
+ })();
319
323
 
320
324
  `,
321
325
  html: `autoScroll: true,
@@ -329,39 +333,7 @@ ${ROOT_PANEL}
329
333
  pins: ${JSON.stringify({ package: manifest.package, executorDigest: manifest.executor.executorDigest, policyDigest: manifest.executor.policyDigest })},
330
334
  user: session,
331
335
  send: ${ns}.webApi(me),
332
- confirmPassword: function (suppliedPassword) {
333
- // Send exactly one PasswordConfirm v2 auth with a masked password field.
334
- return new Promise(function (resolve, reject) {
335
- if (typeof suppliedPassword === "string" && suppliedPassword) {
336
- me.sendWebAPI({api: "SYNO.Core.User.PasswordConfirm", method: "auth", version: 2, params: {password: suppliedPassword}, scope: me,
337
- callback: function(success, response) { suppliedPassword = ""; if (!success) reject({code: "dsm_" + ((response && response.code) || "error")}); else resolve({confirmed: true, token: response && response.SynoConfirmPWToken}); }
338
- });
339
- suppliedPassword = "";
340
- return;
341
- }
342
- var form = document.createElement("form"), label = document.createElement("label"), input = document.createElement("input");
343
- var submit = document.createElement("button"), cancel = document.createElement("button");
344
- label.textContent = "Confirm your DSM administrator password";
345
- input.type = "password"; input.autocomplete = "current-password"; input.required = true;
346
- label.appendChild(input); form.appendChild(label);
347
- submit.type = "submit"; submit.textContent = "Approve"; form.appendChild(submit);
348
- cancel.type = "button"; cancel.textContent = "Cancel"; form.appendChild(cancel);
349
- me.body.dom.appendChild(form); input.focus();
350
- function close() { input.value = ""; if (form.parentNode) form.parentNode.removeChild(form); }
351
- cancel.onclick = function() { close(); resolve({confirmed: false}); };
352
- form.onsubmit = function(event) {
353
- event.preventDefault(); submit.disabled = cancel.disabled = true;
354
- var password = input.value; input.value = "";
355
- me.sendWebAPI({api: "SYNO.Core.User.PasswordConfirm", method: "auth", version: 2, params: {password: password}, scope: me,
356
- callback: function(success, response) {
357
- password = ""; close();
358
- if (!success) { reject({code: "dsm_" + ((response && response.code) || "error")}); return; }
359
- resolve({confirmed: true, token: response && response.SynoConfirmPWToken});
360
- }
361
- });
362
- };
363
- });
364
- },
336
+ confirmPassword: ${ns}.createPasswordConfirm(me),
365
337
  call: ${ns}.call,
366
338
  wait: function (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
367
339
  });
@@ -211,7 +211,11 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
211
211
  if (payload.find(p => p.path === `daemon/payload/${daemon.exec}`)?.mode !== "0755")
212
212
  throw new Error("missing executable");
213
213
  const manifest = { package: info.package, daemon, capabilities: { ...(executor ? { rootOps: true } : {}), ...(background ? { jobs: { worker: 'sha256-v1' } } : {}) } };
214
- if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== daemonConfig(manifest))
214
+ // A config from before timeouts.uninstall pairs with a control that rejects the key.
215
+ const legacy = typeof config.timeouts === "object" && config.timeouts !== null && !("uninstall" in config.timeouts);
216
+ const { uninstall: _, ...legacyTimeouts } = daemon.timeouts;
217
+ const canonical = daemonConfig(legacy ? { ...manifest, daemon: { ...daemon, timeouts: legacyTimeouts } } : manifest);
218
+ if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== canonical)
215
219
  throw new Error("non-canonical daemon config");
216
220
  for (const hook of ["start-stop-status", "preupgrade", "preuninst"]) {
217
221
  if (readFileSync(join(dir, "scripts", hook), "utf8") !== renderStandaloneScript(hook, manifest))