@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.
- package/CHANGELOG.md +234 -1
- package/dist/add.d.ts +4 -1
- package/dist/add.js +14 -9
- package/dist/build-spk/dsm-platforms.json +4 -1
- package/dist/build-spk/gateway.d.ts +1 -1
- package/dist/build-spk/gateway.js +8 -4
- package/dist/build-spk/go-toolchain.d.ts +20 -0
- package/dist/build-spk/go-toolchain.js +44 -0
- package/dist/cli.js +13 -7
- package/dist/deploy/native-install-values.d.ts +17 -0
- package/dist/deploy/native-install-values.js +32 -0
- package/dist/deploy/plan.d.ts +6 -2
- package/dist/deploy/plan.js +57 -6
- package/dist/error-registry.js +12 -6
- package/dist/host/dsm-client.js +53 -17
- package/dist/host/dsm-http.js +24 -2
- package/dist/host/types.d.ts +1 -0
- package/dist/info.js +28 -9
- package/dist/init.js +1 -1
- package/dist/runtime/dsm-version.d.ts +6 -0
- package/dist/runtime/dsm-version.js +8 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +8 -5
- package/dist/standalone/daemon.d.ts +2 -1
- package/dist/standalone/daemon.js +10 -6
- package/dist/standalone/dsm-window.js +6 -34
- package/dist/standalone/inspect-spk.js +5 -1
- package/dist/standalone/native-authorization-window.d.ts +1 -1
- package/dist/standalone/native-authorization-window.js +7 -1
- package/dist/standalone/native-root-window.d.ts +4 -2
- package/dist/standalone/native-root-window.js +33 -18
- package/dist/standalone/root-window.d.ts +4 -1
- package/dist/standalone/root-window.js +143 -33
- package/dist/templates/_daemon/go.mod +1 -1
- package/dist/templates/_daemon/main.go +6 -3
- package/dist/templates/_daemon/main_test.go +21 -1
- package/dist/templates/_daemon/testdata/child.go +3 -0
- package/dist/templates/_executor/README.md +4 -1
- package/dist/templates/_executor/anchor.go +10 -0
- package/dist/templates/_executor/anchor_test.go +40 -5
- package/dist/templates/_executor/digest_review_test.go +2 -2
- package/dist/templates/_executor/dsm.go +40 -3
- package/dist/templates/_executor/go.mod +2 -0
- package/dist/templates/_executor/main.go +11 -5
- package/dist/templates/_executor/managed.go +3 -3
- package/dist/templates/_executor/managed_test.go +3 -3
- package/dist/templates/_executor/path_test.go +18 -0
- package/dist/templates/_executor/pid.go +37 -0
- package/dist/templates/_executor/pid_test.go +113 -0
- package/dist/templates/_executor/platform_other.go +8 -0
- package/dist/templates/_executor/purge_log.go +18 -0
- package/dist/templates/_executor/tasks.go +45 -7
- package/dist/templates/_executor/tasks_dsm71_test.go +114 -0
- package/dist/templates/_executor/tasks_test.go +58 -1
- package/dist/templates/_executor/upgrade_watch_test.go +13 -5
- package/dist/templates/_gateway/go.mod +2 -0
- package/dist/templates/native-go/backend/go.mod +1 -1
- package/dist/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/dist/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/dist/templates/native-go/backend/internal/identity/README.md +5 -1
- package/dist/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/dist/templates/native-go/backend/internal/identity/http.go +1 -1
- package/dist/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/dist/templates/native-go/backend/internal/identity/service.go +1 -1
- package/dist/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/dist/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/dist/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
- package/package.json +1 -1
- package/templates/_daemon/go.mod +1 -1
- package/templates/_daemon/main.go +6 -3
- package/templates/_daemon/main_test.go +21 -1
- package/templates/_daemon/testdata/child.go +3 -0
- package/templates/_executor/README.md +4 -1
- package/templates/_executor/anchor.go +10 -0
- package/templates/_executor/anchor_test.go +40 -5
- package/templates/_executor/digest_review_test.go +2 -2
- package/templates/_executor/dsm.go +40 -3
- package/templates/_executor/go.mod +2 -0
- package/templates/_executor/main.go +11 -5
- package/templates/_executor/managed.go +3 -3
- package/templates/_executor/managed_test.go +3 -3
- package/templates/_executor/path_test.go +18 -0
- package/templates/_executor/pid.go +37 -0
- package/templates/_executor/pid_test.go +113 -0
- package/templates/_executor/platform_other.go +8 -0
- package/templates/_executor/purge_log.go +18 -0
- package/templates/_executor/tasks.go +45 -7
- package/templates/_executor/tasks_dsm71_test.go +114 -0
- package/templates/_executor/tasks_test.go +58 -1
- package/templates/_executor/upgrade_watch_test.go +13 -5
- package/templates/_gateway/go.mod +2 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/templates/native-go/backend/internal/identity/README.md +5 -1
- package/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/templates/native-go/backend/internal/identity/http.go +1 -1
- package/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/templates/native-go/backend/internal/identity/service.go +1 -1
- package/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
package/dist/deploy/plan.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/error-registry.js
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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
|
|
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 名稱相符的受信任憑證。不要關閉憑證驗證。" };
|
package/dist/host/dsm-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
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" });
|
package/dist/host/dsm-http.js
CHANGED
|
@@ -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
|
|
6
|
-
|
|
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
|
}
|
package/dist/host/types.d.ts
CHANGED
|
@@ -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
|
|
135
|
-
const installed =
|
|
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
|
-
|
|
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() == "
|
|
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 {
|
|
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 {
|
|
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, ...
|
|
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, ...
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 === "
|
|
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, ...
|
|
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
|
|
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:
|
|
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
|
-
|
|
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))
|