@synopackageland/cli 0.7.0 → 0.7.2
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 +301 -1
- package/assets/default-icon/README.md +19 -0
- package/assets/default-icon/default-icon-small.svg +9 -0
- package/assets/default-icon/default-icon.svg +10 -0
- package/assets/default-icon/icon_16.png +0 -0
- package/assets/default-icon/icon_24.png +0 -0
- package/assets/default-icon/icon_256.png +0 -0
- package/assets/default-icon/icon_32.png +0 -0
- package/assets/default-icon/icon_48.png +0 -0
- package/assets/default-icon/icon_64.png +0 -0
- package/assets/default-icon/icon_72.png +0 -0
- package/dist/add.d.ts +4 -1
- package/dist/add.js +14 -9
- package/dist/build-spk/dsm-icons.d.ts +31 -2
- package/dist/build-spk/dsm-icons.js +135 -56
- 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/generator.js +17 -3
- package/dist/build-spk/go-toolchain.d.ts +20 -0
- package/dist/build-spk/go-toolchain.js +44 -0
- package/dist/build-spk/png.d.ts +14 -0
- package/dist/build-spk/png.js +161 -0
- package/dist/build-spk/ui-texts.d.ts +20 -0
- package/dist/build-spk/ui-texts.js +42 -0
- package/dist/cli.js +39 -9
- package/dist/contract/i18n.d.ts +5 -0
- package/dist/contract/i18n.js +45 -0
- package/dist/contract/icon.d.ts +5 -0
- package/dist/contract/icon.js +27 -0
- package/dist/contract/validate.js +4 -0
- package/dist/contract/web-launch.js +6 -2
- 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 +15 -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.d.ts +2 -0
- package/dist/standalone/dsm-window.js +26 -40
- package/dist/standalone/i18n.d.ts +12 -0
- package/dist/standalone/i18n.js +30 -0
- package/dist/standalone/inspect-spk.d.ts +5 -0
- package/dist/standalone/inspect-spk.js +41 -2
- package/dist/standalone/manifest.d.ts +5 -6
- package/dist/standalone/manifest.js +21 -34
- package/dist/standalone/native-authorization-window.d.ts +1 -1
- package/dist/standalone/native-authorization-window.js +7 -1
- package/dist/standalone/native-layout.d.ts +2 -0
- package/dist/standalone/native-layout.js +22 -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/standalone/spk.js +7 -4
- 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/binary/dsm-package.yaml +3 -0
- package/dist/templates/compose/synology-app.yaml +3 -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/dsm-package.yaml +3 -0
- 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 +2 -1
- package/schema/synology-app.schema.json +22 -0
- 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/binary/dsm-package.yaml +3 -0
- package/templates/compose/synology-app.yaml +3 -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/dsm-package.yaml +3 -0
- 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/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,4 +1,5 @@
|
|
|
1
1
|
import type { EntryMode, StandaloneManifest } from "./manifest.js";
|
|
2
|
+
import { type AppTitle } from "../build-spk/ui-texts.js";
|
|
2
3
|
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
3
4
|
export declare const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
4
5
|
export declare function cgiUrl(packageId: string): string;
|
|
@@ -16,6 +17,7 @@ export declare const I18N_RUNTIME: string;
|
|
|
16
17
|
export declare function windowScriptName(manifest: Pick<StandaloneManifest, "build">): string;
|
|
17
18
|
/** The launcher fields an entry mode fixes; inspect checks a built SPK against them. */
|
|
18
19
|
export declare function entryLauncherAccess(entry: EntryMode): Record<string, unknown>;
|
|
20
|
+
export declare function appTitle(manifest: StandaloneManifest): AppTitle;
|
|
19
21
|
/** `ui/config`: registers the DSM main-menu app and its window class. */
|
|
20
22
|
export declare function renderUiConfig(manifest: StandaloneManifest): string;
|
|
21
23
|
/**
|
|
@@ -1,9 +1,10 @@
|
|
|
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";
|
|
7
|
+
import { uiTitleFields } from "../build-spk/ui-texts.js";
|
|
7
8
|
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
8
9
|
export const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
9
10
|
export function cgiUrl(packageId) {
|
|
@@ -62,15 +63,20 @@ export function entryLauncherAccess(entry) {
|
|
|
62
63
|
? { allUsers: false }
|
|
63
64
|
: { allUsers: true, grantPrivilege: "all", advanceGrantPrivilege: true };
|
|
64
65
|
}
|
|
66
|
+
export function appTitle(manifest) {
|
|
67
|
+
return { displayname: manifest.displayname, description: manifest.description, info: manifest.i18n?.info };
|
|
68
|
+
}
|
|
65
69
|
/** `ui/config`: registers the DSM main-menu app and its window class. */
|
|
66
70
|
export function renderUiConfig(manifest) {
|
|
67
71
|
const ns = `SYNO.SDS.${manifest.package}`;
|
|
72
|
+
const { title, desc, preloadTexts } = uiTitleFields(appTitle(manifest));
|
|
68
73
|
return `${JSON.stringify({
|
|
69
74
|
[windowScriptName(manifest)]: {
|
|
70
75
|
[`${ns}.Instance`]: {
|
|
71
76
|
type: "app",
|
|
72
|
-
title
|
|
73
|
-
desc
|
|
77
|
+
title,
|
|
78
|
+
desc,
|
|
79
|
+
...(preloadTexts ? { preloadTexts } : {}),
|
|
74
80
|
icon: "images/app_{0}.png",
|
|
75
81
|
appWindow: `${ns}.MainWindow`,
|
|
76
82
|
allowMultiInstance: false,
|
|
@@ -79,7 +85,7 @@ export function renderUiConfig(manifest) {
|
|
|
79
85
|
},
|
|
80
86
|
[`${ns}.MainWindow`]: {
|
|
81
87
|
type: "lib",
|
|
82
|
-
title
|
|
88
|
+
title,
|
|
83
89
|
icon: "images/app_{0}.png",
|
|
84
90
|
depend: [],
|
|
85
91
|
},
|
|
@@ -300,6 +306,14 @@ const ROOT_PANEL_CSS = [
|
|
|
300
306
|
".dsmapp-rootpanel-primary{background:#057feb;color:#fff;border:1px solid #057feb;border-radius:3px}",
|
|
301
307
|
".dsmapp-rootpanel-message{color:#c0392b}.dsmapp-rootpanel-hint{color:#526070;font-size:12px}",
|
|
302
308
|
].join("");
|
|
309
|
+
/**
|
|
310
|
+
* An install-selected delegate scope is approved only in the app's settings: the
|
|
311
|
+
* approval carries the scope the administrator picks there, and the window panel
|
|
312
|
+
* has no scope picker.
|
|
313
|
+
*/
|
|
314
|
+
function settingsOnlyApproval(manifest) {
|
|
315
|
+
return manifest.ui.build !== undefined && manifest.capabilities.delegate?.scope === "install-selected";
|
|
316
|
+
}
|
|
303
317
|
/**
|
|
304
318
|
* The capabilities.rootOps: true additions to the window: the exact tasks and panel
|
|
305
319
|
* text, the access runtime wired to DSM's own password confirmation and the
|
|
@@ -316,6 +330,10 @@ ${ns}.mountRootPanel = (function () {
|
|
|
316
330
|
${ROOT_PANEL}
|
|
317
331
|
return mountRootPanel;
|
|
318
332
|
})();
|
|
333
|
+
${ns}.createPasswordConfirm = (function () {
|
|
334
|
+
${PASSWORD_CONFIRM}
|
|
335
|
+
return createPasswordConfirm;
|
|
336
|
+
})();
|
|
319
337
|
|
|
320
338
|
`,
|
|
321
339
|
html: `autoScroll: true,
|
|
@@ -329,46 +347,14 @@ ${ROOT_PANEL}
|
|
|
329
347
|
pins: ${JSON.stringify({ package: manifest.package, executorDigest: manifest.executor.executorDigest, policyDigest: manifest.executor.policyDigest })},
|
|
330
348
|
user: session,
|
|
331
349
|
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
|
-
},
|
|
350
|
+
confirmPassword: ${ns}.createPasswordConfirm(me),
|
|
365
351
|
call: ${ns}.call,
|
|
366
352
|
wait: function (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
367
353
|
});
|
|
368
|
-
var panels = me.body && me.body.dom.querySelector(".dsmapp-rootpanels");
|
|
369
|
-
if (panels
|
|
354
|
+
${settingsOnlyApproval(manifest) ? "" : ` var panels = me.body && me.body.dom.querySelector(".dsmapp-rootpanels");
|
|
355
|
+
if (panels) {
|
|
370
356
|
${ns}.mountRootPanel(panels, access, null, ${ns}.rootTasks, session().admin);
|
|
371
357
|
}
|
|
372
|
-
`,
|
|
358
|
+
`}`,
|
|
373
359
|
};
|
|
374
360
|
}
|
|
@@ -18,6 +18,18 @@ export type CatalogsResult = {
|
|
|
18
18
|
ok: false;
|
|
19
19
|
errors: StandaloneError[];
|
|
20
20
|
};
|
|
21
|
+
export interface InfoText {
|
|
22
|
+
displayname?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
}
|
|
25
|
+
/** INFO `displayname_<dsm lang>` / `description_<dsm lang>`; English stays top level. */
|
|
26
|
+
export type InfoTranslations = Partial<Record<Locale, InfoText>>;
|
|
27
|
+
export interface InfoTextChecks {
|
|
28
|
+
invalid(why: string): undefined;
|
|
29
|
+
/** The plain single-line text INFO and DSM strings files can carry, or undefined after reporting. */
|
|
30
|
+
text(value: unknown, field: string): string | undefined;
|
|
31
|
+
}
|
|
32
|
+
export declare function readInfoTranslations(value: unknown, check: InfoTextChecks): InfoTranslations | undefined;
|
|
21
33
|
export declare function isLocale(value: string): value is Locale;
|
|
22
34
|
/** `<dir>/<locale>.json` for each shipped locale; en.json is required. */
|
|
23
35
|
export declare function loadCatalogs(dir: string): CatalogsResult;
|
package/dist/standalone/i18n.js
CHANGED
|
@@ -17,6 +17,36 @@ export const DSM_LANGS = {
|
|
|
17
17
|
es: "spn",
|
|
18
18
|
it: "ita",
|
|
19
19
|
};
|
|
20
|
+
const INFO_TEXT_FIELDS = new Set(["displayname", "description"]);
|
|
21
|
+
export function readInfoTranslations(value, check) {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
if (!isRecord(value)) {
|
|
26
|
+
return check.invalid("必須以語言為 key,例如 zh-TW: { displayname, description }。");
|
|
27
|
+
}
|
|
28
|
+
const info = {};
|
|
29
|
+
for (const [locale, text] of Object.entries(value)) {
|
|
30
|
+
if (!isLocale(locale) || locale === "en") {
|
|
31
|
+
return check.invalid(`的「${locale}」不是支援的翻譯語言;英文寫在頂層 displayname / description。`);
|
|
32
|
+
}
|
|
33
|
+
if (!isRecord(text) || Object.keys(text).some((key) => !INFO_TEXT_FIELDS.has(key))) {
|
|
34
|
+
return check.invalid(`.${locale} 只接受 displayname 與 description。`);
|
|
35
|
+
}
|
|
36
|
+
const entry = {};
|
|
37
|
+
for (const field of INFO_TEXT_FIELDS) {
|
|
38
|
+
if (text[field] !== undefined) {
|
|
39
|
+
const checked = check.text(text[field], `i18n.info.${locale}.${field}`);
|
|
40
|
+
if (!checked) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
entry[field] = checked;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
info[locale] = entry;
|
|
47
|
+
}
|
|
48
|
+
return info;
|
|
49
|
+
}
|
|
20
50
|
const MESSAGE_KEY = /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)*$/;
|
|
21
51
|
export function isLocale(value) {
|
|
22
52
|
return LOCALES.includes(value);
|
|
@@ -30,6 +30,11 @@ export interface SpkInspection {
|
|
|
30
30
|
};
|
|
31
31
|
/** Declared `entry` mode; `admin-only` is the administrator floor of every route. */
|
|
32
32
|
entry?: EntryMode;
|
|
33
|
+
/** PACKAGE_ICON_256.PNG: the SDK default, a custom icon (by digest) or none. */
|
|
34
|
+
icon?: {
|
|
35
|
+
source: "default" | "custom" | "none";
|
|
36
|
+
sha256?: string;
|
|
37
|
+
};
|
|
33
38
|
}
|
|
34
39
|
/** Reads an SPK without installing it and reports how it would run on DSM. */
|
|
35
40
|
export declare function inspectStandaloneSpk(spkPath: string): Promise<SpkInspection>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { inspectNativeLayout } from "./native-layout.js";
|
|
2
3
|
import { inspectExecutor, inspectExecutorAnchor } from "../runtime/executor-inspect.js";
|
|
3
4
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
@@ -20,6 +21,8 @@ import { listPortExposures, loadCompose } from "../compose/analyze.js";
|
|
|
20
21
|
import { uninstallCleanup } from "../runtime/uninstall-cleanup.js";
|
|
21
22
|
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, portalResource, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep } from "../portal/package.js";
|
|
22
23
|
import { isPortalAlias } from "../portal/alias.js";
|
|
24
|
+
import { defaultPackageIcons, MAX_ICON_BYTES } from "../build-spk/dsm-icons.js";
|
|
25
|
+
import { decodePng } from "../build-spk/png.js";
|
|
23
26
|
const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
|
|
24
27
|
/** Reads an SPK without installing it and reports how it would run on DSM. */
|
|
25
28
|
export async function inspectStandaloneSpk(spkPath) {
|
|
@@ -41,8 +44,9 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
41
44
|
for (const key of ["package", "version", "arch", "os_min_ver", "checksum"])
|
|
42
45
|
if (!info[key])
|
|
43
46
|
findings.push(`Missing INFO ${key}`);
|
|
47
|
+
const icon = inspectPackageIcons(dir, findings);
|
|
44
48
|
return { kind: "native-layout", package: info.package, version: info.version, arch: info.arch,
|
|
45
|
-
signed: existsSync(join(dir, "syno_signature.asc")), payload, findings, capabilities: [] };
|
|
49
|
+
signed: existsSync(join(dir, "syno_signature.asc")), payload, findings, capabilities: [], icon };
|
|
46
50
|
}
|
|
47
51
|
const payload = await listPayload(join(dir, "package.tgz"), findings);
|
|
48
52
|
const paths = new Set(payload.map(entry => entry.path));
|
|
@@ -112,6 +116,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
112
116
|
await checkEntryLauncher(dir, info.package, metadata.entry, findings);
|
|
113
117
|
if (standalone)
|
|
114
118
|
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === 'install-selected', portal);
|
|
119
|
+
const icon = inspectPackageIcons(dir, findings);
|
|
115
120
|
const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
|
|
116
121
|
if (standalone && !cgiEntry) {
|
|
117
122
|
findings.push(`payload 沒有 ${CGI_PAYLOAD_PATH}`);
|
|
@@ -121,6 +126,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
121
126
|
}
|
|
122
127
|
return {
|
|
123
128
|
kind,
|
|
129
|
+
icon,
|
|
124
130
|
...(executor ? { executor } : {}),
|
|
125
131
|
...(portal ? { portal: { appId: portal.appId, alias: portal.alias } } : {}),
|
|
126
132
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
@@ -156,6 +162,35 @@ async function checkEntryLauncher(dir, pkg, entry, findings) {
|
|
|
156
162
|
findings.push(`ENTRY_LAUNCHER_MISMATCH sdk-package.json 宣告 entry: ${entry},但 ui/config 的 launcher 不是 ${JSON.stringify(expected)};請用 synopkgland build 重新產生`);
|
|
157
163
|
}
|
|
158
164
|
}
|
|
165
|
+
/** Package Center shows PACKAGE_ICON at 64 and 256 px; the package source refuses a release whose icon is over 256 KiB. */
|
|
166
|
+
function inspectPackageIcons(dir, findings) {
|
|
167
|
+
for (const [name, size] of [["PACKAGE_ICON.PNG", 64], ["PACKAGE_ICON_256.PNG", 256]]) {
|
|
168
|
+
const bytes = readOptionalBytes(join(dir, name));
|
|
169
|
+
if (!bytes)
|
|
170
|
+
continue;
|
|
171
|
+
if (bytes.length > MAX_ICON_BYTES) {
|
|
172
|
+
findings.push(`${name} 有 ${bytes.length} bytes,超過 package source 的 256 KiB 上限;上架後整個版本會被拒收`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const image = decodePng(bytes);
|
|
177
|
+
if (image.width !== size || image.height !== size)
|
|
178
|
+
findings.push(`${name} 是 ${image.width}×${image.height},Package Center 需要 ${size}×${size}`);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
findings.push(`${name}:${error.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const big = readOptionalBytes(join(dir, "PACKAGE_ICON_256.PNG"));
|
|
185
|
+
if (!big)
|
|
186
|
+
return { source: "none" };
|
|
187
|
+
return big.equals(defaultPackageIcons().packageIcon256)
|
|
188
|
+
? { source: "default" }
|
|
189
|
+
: { source: "custom", sha256: createHash("sha256").update(big).digest("hex") };
|
|
190
|
+
}
|
|
191
|
+
function readOptionalBytes(path) {
|
|
192
|
+
return existsSync(path) ? readFileSync(path) : undefined;
|
|
193
|
+
}
|
|
159
194
|
const OS_MIN_VER = /^7\.\d+(\.\d+)?-\d+$/;
|
|
160
195
|
/** INFO os_min_ver and arch must be values DSM accepts, and arch must match the shipped CGI. */
|
|
161
196
|
async function checkArch(dir, info, kind, hasCgi, findings) {
|
|
@@ -211,7 +246,11 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
211
246
|
if (payload.find(p => p.path === `daemon/payload/${daemon.exec}`)?.mode !== "0755")
|
|
212
247
|
throw new Error("missing executable");
|
|
213
248
|
const manifest = { package: info.package, daemon, capabilities: { ...(executor ? { rootOps: true } : {}), ...(background ? { jobs: { worker: 'sha256-v1' } } : {}) } };
|
|
214
|
-
|
|
249
|
+
// A config from before timeouts.uninstall pairs with a control that rejects the key.
|
|
250
|
+
const legacy = typeof config.timeouts === "object" && config.timeouts !== null && !("uninstall" in config.timeouts);
|
|
251
|
+
const { uninstall: _, ...legacyTimeouts } = daemon.timeouts;
|
|
252
|
+
const canonical = daemonConfig(legacy ? { ...manifest, daemon: { ...daemon, timeouts: legacyTimeouts } } : manifest);
|
|
253
|
+
if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== canonical)
|
|
215
254
|
throw new Error("non-canonical daemon config");
|
|
216
255
|
for (const hook of ["start-stop-status", "preupgrade", "preuninst"]) {
|
|
217
256
|
if (readFileSync(join(dir, "scripts", hook), "utf8") !== renderStandaloneScript(hook, manifest))
|
|
@@ -2,7 +2,8 @@ import { type PackageMetadata } from "../package-metadata.js";
|
|
|
2
2
|
import { type DelegateCapability } from "../runtime/executor-policy.js";
|
|
3
3
|
import type { ExecutorPins } from "../runtime/executor-tasks.js";
|
|
4
4
|
import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
5
|
-
import { type Catalogs, type
|
|
5
|
+
import { type Catalogs, type InfoTranslations } from "./i18n.js";
|
|
6
|
+
export type { InfoText } from "./i18n.js";
|
|
6
7
|
import { type RootOp } from "./root-ops.js";
|
|
7
8
|
import { type Daemon } from "./daemon.js";
|
|
8
9
|
import { type Background } from "./background.js";
|
|
@@ -22,15 +23,11 @@ export type StandaloneBackend = {
|
|
|
22
23
|
kind: "binary";
|
|
23
24
|
files: Partial<Record<NativeArchFamily, string>>;
|
|
24
25
|
};
|
|
25
|
-
export interface InfoText {
|
|
26
|
-
displayname?: string;
|
|
27
|
-
description?: string;
|
|
28
|
-
}
|
|
29
26
|
export interface StandaloneI18n {
|
|
30
27
|
/** UI messages per locale, embedded into the DSM window. */
|
|
31
28
|
catalogs?: Catalogs;
|
|
32
29
|
/** INFO `displayname_<dsm lang>` / `description_<dsm lang>`. */
|
|
33
|
-
info:
|
|
30
|
+
info: InfoTranslations;
|
|
34
31
|
}
|
|
35
32
|
export interface StandaloneManifest {
|
|
36
33
|
metadata?: PackageMetadata;
|
|
@@ -69,6 +66,8 @@ export interface StandaloneManifest {
|
|
|
69
66
|
entry?: EntryMode;
|
|
70
67
|
/** `portal`: registers the DSM Application Portal entry; an administrator activates it. */
|
|
71
68
|
portal?: PortalDeclaration;
|
|
69
|
+
/** `icon`: a square PNG of at least 256 px, or a directory of icon_<size>.png; absent uses the SDK default. */
|
|
70
|
+
icon?: string;
|
|
72
71
|
}
|
|
73
72
|
/**
|
|
74
73
|
* `admin-only`: DSM administrators only. `app-privilege`: administrators and
|