@synopackageland/cli 0.3.10 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/dist/bin.js +1 -2
- package/dist/build-spk/generator.d.ts +0 -3
- package/dist/build-spk/generator.js +135 -287
- package/dist/build-spk/native-lifecycle.js +0 -3
- package/dist/build-spk/native-payload.js +0 -1
- package/dist/cli.js +171 -151
- package/dist/compose/analyze.d.ts +1 -2
- package/dist/compose/analyze.js +5 -48
- package/dist/contract/unsupported-types.d.ts +10 -0
- package/dist/contract/unsupported-types.js +49 -0
- package/dist/contract/validate.js +25 -2
- package/dist/contract/web-launch.js +0 -2
- package/dist/deploy/deploy.d.ts +1 -4
- package/dist/deploy/deploy.js +45 -156
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/plan.d.ts +18 -0
- package/dist/deploy/plan.js +54 -0
- package/dist/deploy/spk-version.d.ts +2 -14
- package/dist/deploy/spk-version.js +25 -111
- package/dist/dev.d.ts +1 -3
- package/dist/dev.js +14 -28
- package/dist/error-registry.js +810 -862
- package/dist/host/dsm-client.js +12 -272
- package/dist/host/dsm-port-profile.d.ts +0 -1
- package/dist/host/dsm-port-profile.js +0 -2
- package/dist/host/types.d.ts +1 -83
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -6
- package/dist/info.d.ts +0 -1
- package/dist/info.js +3 -10
- package/dist/init.d.ts +11 -1
- package/dist/init.js +112 -4
- package/dist/log.js +2 -3
- package/dist/native/validate.js +0 -1
- package/dist/standalone/binary.d.ts +9 -0
- package/dist/standalone/binary.js +67 -0
- package/dist/standalone/build.d.ts +25 -0
- package/dist/standalone/build.js +108 -0
- package/dist/standalone/dsm-window.d.ts +26 -0
- package/dist/standalone/dsm-window.js +251 -0
- package/dist/standalone/helper.d.ts +31 -0
- package/dist/standalone/helper.js +104 -0
- package/dist/standalone/i18n.d.ts +28 -0
- package/dist/standalone/i18n.js +161 -0
- package/dist/standalone/inspect-spk.d.ts +21 -0
- package/dist/standalone/inspect-spk.js +160 -0
- package/dist/standalone/manifest.d.ts +71 -0
- package/dist/standalone/manifest.js +276 -0
- package/dist/standalone/root-messages.d.ts +7 -0
- package/dist/standalone/root-messages.js +250 -0
- package/dist/standalone/root-ops.d.ts +42 -0
- package/dist/standalone/root-ops.js +101 -0
- package/dist/standalone/root-window.d.ts +35 -0
- package/dist/standalone/root-window.js +290 -0
- package/dist/standalone/spk.d.ts +22 -0
- package/dist/standalone/spk.js +205 -0
- package/dist/templates/binary/synology-app.yaml +7 -0
- package/dist/templates/hello-files/compose.yaml +0 -1
- package/dist/templates/hello-web/compose.yaml +13 -10
- package/dist/templates/hello-web/html/app.js +1 -23
- package/dist/templates/hello-web/html/index.html +1 -3
- package/dist/templates/hello-web-page/compose.yaml +13 -10
- package/dist/templates/hello-web-page/html/app.js +1 -23
- package/dist/templates/hello-web-page/html/index.html +1 -3
- package/dist/templates/native-go/README.md +268 -0
- package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
- package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/dist/templates/native-go/backend/go.mod +3 -0
- package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
- package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
- package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/dist/templates/native-go/backend/internal/api/server.go +155 -0
- package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
- package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
- package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
- package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/dist/templates/native-go/dsm-package.yaml +58 -0
- package/dist/templates/native-go/gitignore +1 -0
- package/dist/templates/native-go/i18n/glossary.json +662 -0
- package/dist/templates/native-go/i18n/messages/de.json +28 -0
- package/dist/templates/native-go/i18n/messages/en.json +28 -0
- package/dist/templates/native-go/i18n/messages/es.json +28 -0
- package/dist/templates/native-go/i18n/messages/fr.json +28 -0
- package/dist/templates/native-go/i18n/messages/it.json +28 -0
- package/dist/templates/native-go/i18n/messages/ja.json +28 -0
- package/dist/templates/native-go/i18n/messages/ko.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/dist/templates/native-go/ui/app.css +35 -0
- package/dist/templates/native-go/ui/app.js +94 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
- package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
- package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/dist/templates/native-go-better-auth/node/package.json +1 -0
- package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/dist/templates/native-go-root/root/whoami.sh +6 -0
- package/dist/templates/native-go-vue/README.md +110 -0
- package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/dist/templates/native-go-vue/frontend/package.json +6 -0
- package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
- package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/dist/templates/native-go-vue/gitignore +2 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +11 -7
- package/package.json +3 -7
- package/schema/synology-app.schema.json +36 -37
- package/skills/SKILL.md +4 -48
- package/skills/reference/create.md +11 -83
- package/skills/reference/deploy.md +7 -78
- package/skills/reference/diagnose.md +4 -101
- package/skills/reference/files.md +5 -41
- package/skills/reference/identity.md +3 -29
- package/skills/reference/wrap.md +0 -2
- package/templates/binary/synology-app.yaml +7 -0
- package/templates/hello-files/compose.yaml +0 -1
- package/templates/hello-web/compose.yaml +13 -10
- package/templates/hello-web/html/app.js +1 -23
- package/templates/hello-web/html/index.html +1 -3
- package/templates/hello-web-page/compose.yaml +13 -10
- package/templates/hello-web-page/html/app.js +1 -23
- package/templates/hello-web-page/html/index.html +1 -3
- package/templates/native-go/README.md +268 -0
- package/templates/native-go/backend/cmd/app/main.go +49 -0
- package/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/templates/native-go/backend/go.mod +3 -0
- package/templates/native-go/backend/internal/api/errors.go +33 -0
- package/templates/native-go/backend/internal/api/registry.go +94 -0
- package/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/templates/native-go/backend/internal/api/server.go +155 -0
- package/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/templates/native-go/backend/internal/app/routes.go +127 -0
- package/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/templates/native-go/backend/internal/helper/control.go +219 -0
- package/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/templates/native-go/backend/internal/helper/status.go +271 -0
- package/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/templates/native-go/dsm-package.yaml +58 -0
- package/templates/native-go/gitignore +1 -0
- package/templates/native-go/i18n/glossary.json +662 -0
- package/templates/native-go/i18n/messages/de.json +28 -0
- package/templates/native-go/i18n/messages/en.json +28 -0
- package/templates/native-go/i18n/messages/es.json +28 -0
- package/templates/native-go/i18n/messages/fr.json +28 -0
- package/templates/native-go/i18n/messages/it.json +28 -0
- package/templates/native-go/i18n/messages/ja.json +28 -0
- package/templates/native-go/i18n/messages/ko.json +28 -0
- package/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/templates/native-go/ui/app.css +35 -0
- package/templates/native-go/ui/app.js +94 -0
- package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/templates/native-go-better-auth/node/app.mjs +72 -0
- package/templates/native-go-better-auth/node/build.mjs +2 -0
- package/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/templates/native-go-better-auth/node/package.json +1 -0
- package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/templates/native-go-root/root/whoami.sh +6 -0
- package/templates/native-go-vue/README.md +110 -0
- package/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/templates/native-go-vue/frontend/package.json +6 -0
- package/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/templates/native-go-vue/frontend/src/style.css +14 -0
- package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/templates/native-go-vue/gitignore +2 -0
- package/dist/attestation/app-package.d.ts +0 -86
- package/dist/attestation/app-package.js +0 -206
- package/dist/attestation/install-on-host.d.ts +0 -14
- package/dist/attestation/install-on-host.js +0 -52
- package/dist/attestation/keyring.d.ts +0 -21
- package/dist/attestation/keyring.js +0 -168
- package/dist/attestation/platform-service.d.ts +0 -70
- package/dist/attestation/platform-service.js +0 -250
- package/dist/build-spk/broker-glue.d.ts +0 -16
- package/dist/build-spk/broker-glue.js +0 -40
- package/dist/deploy/catalog-publish.d.ts +0 -3
- package/dist/deploy/catalog-publish.js +0 -7
- package/dist/doctor.d.ts +0 -22
- package/dist/doctor.js +0 -822
- package/dist/error-collector.d.ts +0 -2
- package/dist/error-collector.js +0 -46
- package/dist/host/broker-paths.d.ts +0 -1
- package/dist/host/broker-paths.js +0 -1
- package/dist/host/broker-upgrade-preflight.d.ts +0 -61
- package/dist/host/broker-upgrade-preflight.js +0 -156
- package/dist/host/digests.d.ts +0 -5
- package/dist/host/digests.js +0 -63
- package/dist/host/fake-nas.d.ts +0 -49
- package/dist/host/fake-nas.js +0 -193
- package/dist/mock-broker/bootstrap.d.ts +0 -2
- package/dist/mock-broker/bootstrap.js +0 -8
- package/dist/mock-broker/files.d.ts +0 -60
- package/dist/mock-broker/files.js +0 -408
- package/dist/mock-broker/oidc.d.ts +0 -31
- package/dist/mock-broker/oidc.js +0 -161
- package/dist/mock-broker/path.d.ts +0 -9
- package/dist/mock-broker/path.js +0 -50
- package/dist/mock-broker/server.d.ts +0 -19
- package/dist/mock-broker/server.js +0 -350
- package/dist/publish/build-ledger.d.ts +0 -22
- package/dist/publish/build-ledger.js +0 -26
- package/dist/publish/community-path.d.ts +0 -6
- package/dist/publish/community-path.js +0 -41
- package/dist/publish/curated-release.d.ts +0 -78
- package/dist/publish/curated-release.js +0 -883
- package/dist/publish/file-build-ledger.d.ts +0 -18
- package/dist/publish/file-build-ledger.js +0 -268
- package/dist/publish/github-pr.d.ts +0 -25
- package/dist/publish/github-pr.js +0 -85
- package/dist/publish/github-repo.d.ts +0 -8
- package/dist/publish/github-repo.js +0 -21
- package/dist/publish/platform-service-release.d.ts +0 -60
- package/dist/publish/platform-service-release.js +0 -333
- package/dist/publish/preflight.d.ts +0 -24
- package/dist/publish/preflight.js +0 -138
- package/dist/publish/publish.d.ts +0 -27
- package/dist/publish/publish.js +0 -136
- package/dist/publish/sync-definition.d.ts +0 -4
- package/dist/publish/sync-definition.js +0 -48
- package/dist/release-bin.d.ts +0 -2
- package/dist/release-bin.js +0 -23
- package/dist/templates/hello-web/html/sdk.js +0 -68
- package/dist/templates/hello-web-page/html/sdk.js +0 -68
- package/dist/templates/native/synology-app.yaml +0 -7
- package/dist/test-host.d.ts +0 -15
- package/dist/test-host.js +0 -37
- package/templates/hello-web/html/sdk.js +0 -68
- package/templates/hello-web-page/html/sdk.js +0 -68
- package/templates/native/synology-app.yaml +0 -7
- /package/dist/templates/{native → binary}/native.yaml +0 -0
- /package/templates/{native → binary}/native.yaml +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
import type { CommandRunner } from "./build.js";
|
|
3
|
+
import type { StandaloneError, StandaloneManifest } from "./manifest.js";
|
|
4
|
+
import { type RootOp } from "./root-ops.js";
|
|
5
|
+
export interface Integration {
|
|
6
|
+
protocol: number;
|
|
7
|
+
workers: Record<string, string>;
|
|
8
|
+
identity?: "better-auth";
|
|
9
|
+
}
|
|
10
|
+
export interface HelperInventory {
|
|
11
|
+
package: string;
|
|
12
|
+
version: string;
|
|
13
|
+
protocol: number;
|
|
14
|
+
payloadDigest: string;
|
|
15
|
+
registryDigest: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WorkerPayload {
|
|
18
|
+
id: string;
|
|
19
|
+
digest: string;
|
|
20
|
+
bytes: Buffer;
|
|
21
|
+
}
|
|
22
|
+
export interface HelperArtifact {
|
|
23
|
+
binary: string;
|
|
24
|
+
inventory: HelperInventory;
|
|
25
|
+
workers: WorkerPayload[];
|
|
26
|
+
ops: RootOp[];
|
|
27
|
+
}
|
|
28
|
+
export declare function readIntegration(raw: unknown, root: string, fail: (e: StandaloneError) => void): Integration | undefined;
|
|
29
|
+
export declare function assertHelperTaskBudget(script: string, metadata?: string): void;
|
|
30
|
+
export declare function helperOps(m: StandaloneManifest, inventory: HelperInventory): RootOp[];
|
|
31
|
+
export declare function buildHelper(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): HelperArtifact | undefined;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { renderRootTaskScript } from "./root-ops.js";
|
|
5
|
+
import { StandaloneBuildError } from "./spk.js";
|
|
6
|
+
const sha256 = (b) => createHash("sha256").update(b).digest("hex");
|
|
7
|
+
const record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8
|
+
export function readIntegration(raw, root, fail) {
|
|
9
|
+
if (raw === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
const invalid = () => { fail({ code: "STANDALONE_INTEGRATION_INVALID", message: "integration 需要 protocol: 1 與 workers: {固定 ID: 專案內檔案};最多 16 個,僅作 inventory,不執行 worker。" }); return undefined; };
|
|
12
|
+
if (!record(raw) || Object.keys(raw).some(k => !["protocol", "workers", "identity"].includes(k)) || raw.protocol !== 1 || !record(raw.workers) || Object.keys(raw.workers).length > 16)
|
|
13
|
+
return invalid();
|
|
14
|
+
if (raw.identity !== undefined && raw.identity !== "better-auth")
|
|
15
|
+
return invalid();
|
|
16
|
+
const workers = {};
|
|
17
|
+
for (const [id, file] of Object.entries(raw.workers)) {
|
|
18
|
+
if (!/^[a-z][a-z0-9_]{0,31}$/.test(id) || typeof file !== "string" || !file || isAbsolute(file) || file.split(/[\\/]/).includes(".."))
|
|
19
|
+
return invalid();
|
|
20
|
+
const path = resolve(root, file), rel = relative(root, path);
|
|
21
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
22
|
+
return invalid();
|
|
23
|
+
workers[id] = path;
|
|
24
|
+
}
|
|
25
|
+
return { protocol: 1, workers, ...(raw.identity === "better-auth" ? { identity: "better-auth" } : {}) };
|
|
26
|
+
}
|
|
27
|
+
function snapshotWorkers(m) {
|
|
28
|
+
let total = 0;
|
|
29
|
+
return Object.entries(m.integration.workers).sort(([a], [b]) => a.localeCompare(b, "en")).map(([id, path]) => {
|
|
30
|
+
const rel = relative(realpathSync(m.root), realpathSync(path));
|
|
31
|
+
if (rel.startsWith("..") || isAbsolute(rel) || !lstatSync(path).isFile())
|
|
32
|
+
throw new StandaloneBuildError("HELPER_WORKER_UNSAFE", `${id} 必須是專案內的一般檔案,不接受 symlink。`);
|
|
33
|
+
const bytes = readFileSync(path);
|
|
34
|
+
total += bytes.length;
|
|
35
|
+
if (total > 1024 * 1024)
|
|
36
|
+
throw new StandaloneBuildError("HELPER_WORKERS_TOO_LARGE", "worker inventory payload 合計不可超過 1 MiB。");
|
|
37
|
+
return { id, digest: sha256(bytes), bytes };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function assertHelperTaskBudget(script, metadata = JSON.stringify({ script })) {
|
|
41
|
+
if (Buffer.byteLength(script) > 4095)
|
|
42
|
+
throw new StandaloneBuildError("HELPER_TASK_TOO_LARGE", "helper task 超過 DSM 文件的 4095 bytes 保守限制。");
|
|
43
|
+
if (Buffer.byteLength(metadata) > 8191)
|
|
44
|
+
throw new StandaloneBuildError("HELPER_METADATA_TOO_LARGE", "helper scheduler metadata 超過 8191 bytes 有界限制。");
|
|
45
|
+
}
|
|
46
|
+
export function helperOps(m, inventory) {
|
|
47
|
+
return ["enable", "recover", "teardown", "disable"].map(action => {
|
|
48
|
+
const op = { name: `helper_${action}`, protectedOutput: true, body: `#!/bin/sh
|
|
49
|
+
set -eu
|
|
50
|
+
umask 077
|
|
51
|
+
[ "$(id -u)" = 0 ]
|
|
52
|
+
# Verify the installed version as well as the window's exact-script check.
|
|
53
|
+
info=/var/packages/${m.package}/INFO
|
|
54
|
+
[ ! -L "$info" ] && [ "$(stat -c %u "$info")" = 0 ]
|
|
55
|
+
grep -Fqx 'version="${m.spkVersion}"' "$info"
|
|
56
|
+
for p in /var/lib /run; do
|
|
57
|
+
[ ! -L "$p" ] && [ -d "$p" ] && [ "$(stat -c %u "$p")" = 0 ]
|
|
58
|
+
mode=$(stat -c %a "$p")
|
|
59
|
+
[ "$((0$mode & 0022))" = 0 ]
|
|
60
|
+
done
|
|
61
|
+
s=/var/lib/synopkgland-${m.package}
|
|
62
|
+
r=/run/synopkgland-helper-${m.package}
|
|
63
|
+
for p in "$s" "$r"; do
|
|
64
|
+
[ ! -L "$p" ]
|
|
65
|
+
if [ ! -e "$p" ]; then mkdir -m 0755 "$p"; fi
|
|
66
|
+
[ -d "$p" ] && [ "$(stat -c %u:%a "$p")" = 0:755 ]
|
|
67
|
+
done
|
|
68
|
+
# Copy into a root-owned private file FIRST, then verify the copied bytes.
|
|
69
|
+
# The package may change its source during copy; no unverified byte executes.
|
|
70
|
+
tmp=$(mktemp "$s/.payload.XXXXXX")
|
|
71
|
+
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
|
72
|
+
dd if=/var/packages/${m.package}/target/integration/helper of="$tmp" bs=65536 count=1025 2>/dev/null
|
|
73
|
+
[ "$(stat -c %s "$tmp")" -le 67108864 ]
|
|
74
|
+
chmod 0700 "$tmp"
|
|
75
|
+
[ "$(sha256sum "$tmp" | cut -d ' ' -f 1)" = '${inventory.payloadDigest}' ]
|
|
76
|
+
exe="$s/payload-${inventory.payloadDigest}"
|
|
77
|
+
# Rename only inside the protected directory. Existing executable mappings
|
|
78
|
+
# retain their inode and digest until the controller has verified teardown.
|
|
79
|
+
mv -f "$tmp" "$exe"
|
|
80
|
+
trap - EXIT HUP INT TERM
|
|
81
|
+
exec "$exe" ${action}
|
|
82
|
+
` };
|
|
83
|
+
const script = renderRootTaskScript(m, op);
|
|
84
|
+
assertHelperTaskBudget(script, JSON.stringify({ name: `${m.package} root: ${op.name}`, script, owner: "root", enable: false }));
|
|
85
|
+
return op;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function buildHelper(m, arch, work, run) {
|
|
89
|
+
if (!m.integration)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (m.backend.kind !== "go")
|
|
92
|
+
throw new StandaloneBuildError("HELPER_GO_REQUIRED", "M1a integration 僅支援 native-go。");
|
|
93
|
+
const workers = snapshotWorkers(m);
|
|
94
|
+
const registry = workers.map(({ id, digest }) => ({ id, digest }));
|
|
95
|
+
const base = { package: m.package, version: m.spkVersion, protocol: m.integration.protocol, payloadDigest: "", registryDigest: sha256(JSON.stringify(registry)) };
|
|
96
|
+
const binary = join(work, `helper-${arch}`);
|
|
97
|
+
const result = run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", `-s -w -buildid= -X 'main.helperInventory=${JSON.stringify(base)}'`, "-o", binary, "./cmd/helper"], {
|
|
98
|
+
cwd: m.backend.dir, env: { ...process.env, CGO_ENABLED: "0", GOOS: "linux", GOARCH: arch === "x86_64" ? "amd64" : "arm64" },
|
|
99
|
+
});
|
|
100
|
+
if (result.missing || result.status !== 0)
|
|
101
|
+
throw new StandaloneBuildError("HELPER_BUILD_FAILED", `Go helper build 失敗:${result.stderr}`);
|
|
102
|
+
const inventory = { ...base, payloadDigest: sha256(readFileSync(binary)) };
|
|
103
|
+
return { binary, inventory, workers, ops: helperOps(m, inventory) };
|
|
104
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StandaloneError } from "./manifest.js";
|
|
2
|
+
/**
|
|
3
|
+
* UI languages a Standalone Package can ship, as BCP-47 tags (the catalog
|
|
4
|
+
* file names and the `Intl` locale), with the DSM language code each one
|
|
5
|
+
* answers to. Every other DSM language opens the app in English.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LOCALES: readonly ["en", "zh-TW", "zh-CN", "ja", "ko", "de", "fr", "es", "it"];
|
|
8
|
+
export type Locale = (typeof LOCALES)[number];
|
|
9
|
+
export declare const DSM_LANGS: Record<Locale, string>;
|
|
10
|
+
export type Catalog = Record<string, string>;
|
|
11
|
+
export type Catalogs = {
|
|
12
|
+
en: Catalog;
|
|
13
|
+
} & Partial<Record<Locale, Catalog>>;
|
|
14
|
+
export type CatalogsResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
catalogs: Catalogs;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
errors: StandaloneError[];
|
|
20
|
+
};
|
|
21
|
+
export declare function isLocale(value: string): value is Locale;
|
|
22
|
+
/** `<dir>/<locale>.json` for each shipped locale; en.json is required. */
|
|
23
|
+
export declare function loadCatalogs(dir: string): CatalogsResult;
|
|
24
|
+
/**
|
|
25
|
+
* Each glossary term lists the message keys that must use it: in every
|
|
26
|
+
* shipped locale the message has to contain that locale's official term.
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkGlossary(file: string, catalogs: Catalogs): StandaloneError[];
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* UI languages a Standalone Package can ship, as BCP-47 tags (the catalog
|
|
5
|
+
* file names and the `Intl` locale), with the DSM language code each one
|
|
6
|
+
* answers to. Every other DSM language opens the app in English.
|
|
7
|
+
*/
|
|
8
|
+
export const LOCALES = ["en", "zh-TW", "zh-CN", "ja", "ko", "de", "fr", "es", "it"];
|
|
9
|
+
export const DSM_LANGS = {
|
|
10
|
+
en: "enu",
|
|
11
|
+
"zh-TW": "cht",
|
|
12
|
+
"zh-CN": "chs",
|
|
13
|
+
ja: "jpn",
|
|
14
|
+
ko: "krn",
|
|
15
|
+
de: "ger",
|
|
16
|
+
fr: "fre",
|
|
17
|
+
es: "spn",
|
|
18
|
+
it: "ita",
|
|
19
|
+
};
|
|
20
|
+
const MESSAGE_KEY = /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)*$/;
|
|
21
|
+
export function isLocale(value) {
|
|
22
|
+
return LOCALES.includes(value);
|
|
23
|
+
}
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function placeholders(text) {
|
|
28
|
+
return [...new Set([...text.matchAll(/\{(\w+)\}/g)].map((m) => m[1]))].sort().join(",");
|
|
29
|
+
}
|
|
30
|
+
function readCatalog(file, locale) {
|
|
31
|
+
const invalid = (why) => ({
|
|
32
|
+
error: { code: "STANDALONE_I18N_CATALOG_INVALID", message: `${locale}.json ${why}` },
|
|
33
|
+
});
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return invalid(`不是合法的 JSON:${String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
if (!isRecord(raw)) {
|
|
42
|
+
return invalid("必須是 { \"key\": \"text\" } 的 JSON 物件。");
|
|
43
|
+
}
|
|
44
|
+
for (const [key, text] of Object.entries(raw)) {
|
|
45
|
+
if (!MESSAGE_KEY.test(key) || typeof text !== "string") {
|
|
46
|
+
return invalid(`的「${key}」必須是點分隔的 key(例如 greeting.save)對應一段文字。`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { catalog: raw };
|
|
50
|
+
}
|
|
51
|
+
/** `<dir>/<locale>.json` for each shipped locale; en.json is required. */
|
|
52
|
+
export function loadCatalogs(dir) {
|
|
53
|
+
const errors = [];
|
|
54
|
+
const catalogs = {};
|
|
55
|
+
const files = existsSync(dir) ? readdirSync(dir).filter((name) => name.endsWith(".json")).sort() : [];
|
|
56
|
+
for (const name of files) {
|
|
57
|
+
const locale = name.slice(0, -".json".length);
|
|
58
|
+
if (!isLocale(locale)) {
|
|
59
|
+
errors.push({
|
|
60
|
+
code: "STANDALONE_I18N_LOCALE_UNSUPPORTED",
|
|
61
|
+
message: `${name}:只支援 ${LOCALES.join(", ")};其他 DSM 語言會顯示英文。`,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const read = readCatalog(join(dir, name), locale);
|
|
66
|
+
if ("error" in read) {
|
|
67
|
+
errors.push(read.error);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
catalogs[locale] = read.catalog;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const en = catalogs.en;
|
|
74
|
+
if (!en) {
|
|
75
|
+
if (!errors.some((e) => e.code === "STANDALONE_I18N_CATALOG_INVALID")) {
|
|
76
|
+
errors.push({ code: "STANDALONE_I18N_MESSAGES_MISSING", message: `${dir} 缺少 en.json(所有語言的後備文字)。` });
|
|
77
|
+
}
|
|
78
|
+
return { ok: false, errors };
|
|
79
|
+
}
|
|
80
|
+
for (const [locale, catalog] of Object.entries(catalogs)) {
|
|
81
|
+
errors.push(...compareWithEnglish(locale, catalog, en));
|
|
82
|
+
}
|
|
83
|
+
return errors.length === 0 ? { ok: true, catalogs: catalogs } : { ok: false, errors };
|
|
84
|
+
}
|
|
85
|
+
function compareWithEnglish(locale, catalog, en) {
|
|
86
|
+
const missing = Object.keys(en).filter((key) => !(key in catalog));
|
|
87
|
+
const extra = Object.keys(catalog).filter((key) => !(key in en));
|
|
88
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
89
|
+
const parts = [
|
|
90
|
+
missing.length > 0 ? `缺少 ${missing.join(", ")}` : "",
|
|
91
|
+
extra.length > 0 ? `多出 ${extra.join(", ")}` : "",
|
|
92
|
+
].filter(Boolean);
|
|
93
|
+
return [{ code: "STANDALONE_I18N_KEYS_MISMATCH", message: `${locale}.json 與 en.json 的 key 不同:${parts.join(";")}。` }];
|
|
94
|
+
}
|
|
95
|
+
const drifted = Object.keys(en).filter((key) => placeholders(catalog[key]) !== placeholders(en[key]));
|
|
96
|
+
return drifted.length === 0
|
|
97
|
+
? []
|
|
98
|
+
: [{
|
|
99
|
+
code: "STANDALONE_I18N_PLACEHOLDER_MISMATCH",
|
|
100
|
+
message: `${locale}.json 的 ${drifted.join(", ")} 與 en.json 的 {placeholder} 不一致。`,
|
|
101
|
+
}];
|
|
102
|
+
}
|
|
103
|
+
function readGlossary(file) {
|
|
104
|
+
const invalid = (why) => ({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `${file}:${why}` });
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return invalid(`不是合法的 JSON:${String(error)}`);
|
|
111
|
+
}
|
|
112
|
+
if (!isRecord(raw) || !Array.isArray(raw.terms)) {
|
|
113
|
+
return invalid("必須是 { \"terms\": [...] }。");
|
|
114
|
+
}
|
|
115
|
+
const terms = [];
|
|
116
|
+
for (const term of raw.terms) {
|
|
117
|
+
const ok = isRecord(term)
|
|
118
|
+
&& typeof term.id === "string"
|
|
119
|
+
&& isRecord(term.text)
|
|
120
|
+
&& Object.entries(term.text).every(([locale, text]) => isLocale(locale) && typeof text === "string" && text !== "")
|
|
121
|
+
&& (term.keys === undefined || (Array.isArray(term.keys) && term.keys.every((key) => typeof key === "string")));
|
|
122
|
+
if (!ok) {
|
|
123
|
+
return invalid(`term ${isRecord(term) ? JSON.stringify(term.id) : "?"} 需要 id、以支援語言為 key 的 text,以及選填的 keys 清單。`);
|
|
124
|
+
}
|
|
125
|
+
terms.push({ id: term.id, text: term.text, keys: term.keys ?? [] });
|
|
126
|
+
}
|
|
127
|
+
return terms;
|
|
128
|
+
}
|
|
129
|
+
/** Case-insensitive: a term can start a sentence or sit inside one ("Backend status"). */
|
|
130
|
+
function containsTerm(message, term, locale) {
|
|
131
|
+
return message.toLocaleLowerCase(locale).includes(term.toLocaleLowerCase(locale));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Each glossary term lists the message keys that must use it: in every
|
|
135
|
+
* shipped locale the message has to contain that locale's official term.
|
|
136
|
+
*/
|
|
137
|
+
export function checkGlossary(file, catalogs) {
|
|
138
|
+
const terms = readGlossary(file);
|
|
139
|
+
if (!Array.isArray(terms)) {
|
|
140
|
+
return [terms];
|
|
141
|
+
}
|
|
142
|
+
const errors = [];
|
|
143
|
+
for (const term of terms) {
|
|
144
|
+
for (const key of term.keys) {
|
|
145
|
+
if (!(key in catalogs.en)) {
|
|
146
|
+
errors.push({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `glossary term ${term.id} 指向不存在的 key ${key}。` });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
for (const [locale, catalog] of Object.entries(catalogs)) {
|
|
150
|
+
const official = term.text[locale];
|
|
151
|
+
if (official !== undefined && !containsTerm(catalog[key], official, locale)) {
|
|
152
|
+
errors.push({
|
|
153
|
+
code: "STANDALONE_I18N_GLOSSARY_MISMATCH",
|
|
154
|
+
message: `${locale}.json 的 ${key}「${catalog[key]}」沒有使用 Synology 用語「${official}」(glossary term ${term.id})。`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return errors;
|
|
161
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SpkKind = "native-go" | "binary" | "container";
|
|
2
|
+
export interface SpkInspection {
|
|
3
|
+
kind?: SpkKind;
|
|
4
|
+
package?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
arch?: string;
|
|
7
|
+
/** Whether the SPK carries a Synology package signature. */
|
|
8
|
+
signed: boolean;
|
|
9
|
+
/** Payload path of the package CGI, when present with mode 0755. */
|
|
10
|
+
cgi?: string;
|
|
11
|
+
payload: Array<{
|
|
12
|
+
path: string;
|
|
13
|
+
mode: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** Anything that breaks the unsigned, package-account-only contract. */
|
|
16
|
+
findings: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Reads an SPK without installing it and reports how it would run on DSM. */
|
|
19
|
+
export declare function inspectStandaloneSpk(spkPath: string): Promise<SpkInspection>;
|
|
20
|
+
/** Auto-detect the generated runtime; retain strict native-go inspection when requested. */
|
|
21
|
+
export declare function inspectSpk(spkPath: string, expectedKind?: SpkKind): Promise<SpkInspection>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import * as tar from "tar";
|
|
5
|
+
import { CGI_PAYLOAD_PATH } from "./dsm-window.js";
|
|
6
|
+
const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
|
|
7
|
+
/** Reads an SPK without installing it and reports how it would run on DSM. */
|
|
8
|
+
export async function inspectStandaloneSpk(spkPath) {
|
|
9
|
+
return inspectSpk(spkPath, "native-go");
|
|
10
|
+
}
|
|
11
|
+
/** Auto-detect the generated runtime; retain strict native-go inspection when requested. */
|
|
12
|
+
export async function inspectSpk(spkPath, expectedKind) {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), "syno-inspect-spk-"));
|
|
14
|
+
try {
|
|
15
|
+
await tar.x({ file: spkPath, cwd: dir });
|
|
16
|
+
const findings = [];
|
|
17
|
+
const payload = await listPayload(join(dir, "package.tgz"), findings);
|
|
18
|
+
const paths = new Set(payload.map(entry => entry.path));
|
|
19
|
+
const kind = expectedKind ?? (paths.has(CGI_PAYLOAD_PATH) ? "native-go"
|
|
20
|
+
: paths.has("native.yaml") ? "binary" : paths.has("compose.yaml") ? "container" : "native-go");
|
|
21
|
+
const identityPayload = [CGI_PAYLOAD_PATH, "integration/helper", "integration/inventory.json", "integration/workers.json", "integration/identity/app.mjs"].every(path => paths.has(path));
|
|
22
|
+
const info = readInfo(join(dir, "INFO"), findings, kind, identityPayload);
|
|
23
|
+
checkConf(dir, findings, kind);
|
|
24
|
+
checkScripts(dir, findings);
|
|
25
|
+
const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
|
|
26
|
+
if (kind === "native-go" && !cgiEntry) {
|
|
27
|
+
findings.push(`payload 沒有 ${CGI_PAYLOAD_PATH}`);
|
|
28
|
+
}
|
|
29
|
+
else if (cgiEntry && cgiEntry.mode !== "0755") {
|
|
30
|
+
findings.push(`${CGI_PAYLOAD_PATH} 的 mode 是 ${cgiEntry.mode},應為 0755`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
kind,
|
|
34
|
+
package: info.package,
|
|
35
|
+
version: info.version,
|
|
36
|
+
arch: info.arch,
|
|
37
|
+
signed: existsSync(join(dir, "syno_signature.asc")),
|
|
38
|
+
cgi: cgiEntry?.mode === "0755" ? CGI_PAYLOAD_PATH : undefined,
|
|
39
|
+
payload,
|
|
40
|
+
findings,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function readInfo(path, findings, kind, identityPayload) {
|
|
48
|
+
if (!existsSync(path)) {
|
|
49
|
+
findings.push("缺少 INFO");
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
const info = {};
|
|
53
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
54
|
+
const match = line.match(/^([a-z_]+)="(.*)"$/);
|
|
55
|
+
if (match) {
|
|
56
|
+
info[match[1]] = match[2];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const key of ["package", "version", "arch", "os_min_ver"]) {
|
|
60
|
+
if (!info[key]) {
|
|
61
|
+
findings.push(`INFO 缺少 ${key}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (kind === "native-go" && (info.thirdparty !== "yes" || info.dsmuidir !== "ui")) {
|
|
65
|
+
findings.push('INFO 必須有 thirdparty="yes" 與 dsmuidir="ui",DSM 才會提供 /webman/3rdparty/<package>/');
|
|
66
|
+
}
|
|
67
|
+
const allowedDependency = kind === "container"
|
|
68
|
+
? info.install_dep_packages?.split(":").every(dep => /^(WebStation|ContainerManager)([><=].*)?$/.test(dep))
|
|
69
|
+
: kind === "native-go" && identityPayload && info.install_dep_packages === "Node.js_v22";
|
|
70
|
+
if (info.install_dep_packages && !allowedDependency) {
|
|
71
|
+
findings.push(`INFO 依賴其他套件:${info.install_dep_packages}`);
|
|
72
|
+
}
|
|
73
|
+
return info;
|
|
74
|
+
}
|
|
75
|
+
function checkConf(dir, findings, kind) {
|
|
76
|
+
const confDir = join(dir, "conf");
|
|
77
|
+
const entries = existsSync(confDir) ? readdirSync(confDir) : [];
|
|
78
|
+
for (const name of entries) {
|
|
79
|
+
if (name !== "privilege" && !(kind === "container" && name === "resource")) {
|
|
80
|
+
findings.push(`conf/${name} 會向 DSM 註冊額外資源;Standalone Package 不產生它`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (kind === "container") {
|
|
84
|
+
try {
|
|
85
|
+
const resource = JSON.parse(readFileSync(join(confDir, "resource"), "utf8"));
|
|
86
|
+
if (!resource["docker-project"])
|
|
87
|
+
findings.push("container 缺少 docker-project resource");
|
|
88
|
+
for (const key of Object.keys(resource)) {
|
|
89
|
+
if (!["docker-project", "webservice"].includes(key))
|
|
90
|
+
findings.push(`未知 container resource: ${key}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
findings.push("container 缺少有效的 conf/resource");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!entries.includes("privilege")) {
|
|
98
|
+
findings.push("缺少 conf/privilege(DSM 7 必須以 package 帳號執行)");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let privilege;
|
|
102
|
+
try {
|
|
103
|
+
privilege = JSON.parse(readFileSync(join(confDir, "privilege"), "utf8"));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
findings.push("conf/privilege 不是 JSON");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const defaults = privilege.defaults;
|
|
110
|
+
if (defaults?.["run-as"] !== "package") {
|
|
111
|
+
findings.push('conf/privilege 的 defaults.run-as 必須是 "package"');
|
|
112
|
+
}
|
|
113
|
+
for (const key of ["tool", "ctrl-script", "executable", "join-groupname"]) {
|
|
114
|
+
if (key in privilege) {
|
|
115
|
+
findings.push(`conf/privilege 要求 ${key}(root 或額外群組權限)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function checkScripts(dir, findings) {
|
|
120
|
+
const scriptsDir = join(dir, "scripts");
|
|
121
|
+
if (!existsSync(join(scriptsDir, "start-stop-status"))) {
|
|
122
|
+
findings.push("缺少 scripts/start-stop-status");
|
|
123
|
+
}
|
|
124
|
+
for (const name of existsSync(scriptsDir) ? readdirSync(scriptsDir) : []) {
|
|
125
|
+
const body = readFileSync(join(scriptsDir, name), "utf8");
|
|
126
|
+
if (ROOT_SCRIPT_HINTS.some((pattern) => pattern.test(body))) {
|
|
127
|
+
findings.push(`scripts/${name} 嘗試取得 root 權限(sudo、chown root 或 setuid)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function listPayload(packageTgz, findings) {
|
|
132
|
+
if (!existsSync(packageTgz)) {
|
|
133
|
+
findings.push("缺少 package.tgz");
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const payload = [];
|
|
137
|
+
await tar.t({
|
|
138
|
+
file: packageTgz,
|
|
139
|
+
onReadEntry: (entry) => {
|
|
140
|
+
const path = entry.path.replace(/^\.\//, "");
|
|
141
|
+
const mode = (entry.mode ?? 0) & 0o7777;
|
|
142
|
+
if (entry.type === "SymbolicLink" || entry.type === "Link") {
|
|
143
|
+
findings.push(`payload 含連結:${path}`);
|
|
144
|
+
}
|
|
145
|
+
if (path.startsWith("/") || path.split("/").includes("..")) {
|
|
146
|
+
findings.push(`payload 路徑跳出套件目錄:${path}`);
|
|
147
|
+
}
|
|
148
|
+
if (mode & 0o6000) {
|
|
149
|
+
findings.push(`payload 含 setuid/setgid 檔案:${path}`);
|
|
150
|
+
}
|
|
151
|
+
if (mode & 0o002) {
|
|
152
|
+
findings.push(`payload 含所有人可寫的檔案:${path}`);
|
|
153
|
+
}
|
|
154
|
+
if (entry.type === "File") {
|
|
155
|
+
payload.push({ path, mode: mode.toString(8).padStart(4, "0") });
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return payload;
|
|
160
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
import { type Catalogs, type Locale } from "./i18n.js";
|
|
3
|
+
import { type RootOp } from "./root-ops.js";
|
|
4
|
+
import { type Integration } from "./helper.js";
|
|
5
|
+
/**
|
|
6
|
+
* `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
|
|
7
|
+
* runs as its own DSM package account with a CGI backend and a DSM window UI.
|
|
8
|
+
*/
|
|
9
|
+
export declare const STANDALONE_MANIFEST = "dsm-package.yaml";
|
|
10
|
+
export type StandaloneBackend = {
|
|
11
|
+
kind: "go";
|
|
12
|
+
dir: string;
|
|
13
|
+
main: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "binary";
|
|
16
|
+
files: Partial<Record<NativeArchFamily, string>>;
|
|
17
|
+
};
|
|
18
|
+
export interface InfoText {
|
|
19
|
+
displayname?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface StandaloneI18n {
|
|
23
|
+
/** UI messages per locale, embedded into the DSM window. */
|
|
24
|
+
catalogs?: Catalogs;
|
|
25
|
+
/** INFO `displayname_<dsm lang>` / `description_<dsm lang>`. */
|
|
26
|
+
info: Partial<Record<Locale, InfoText>>;
|
|
27
|
+
}
|
|
28
|
+
export interface StandaloneManifest {
|
|
29
|
+
root: string;
|
|
30
|
+
package: string;
|
|
31
|
+
version: string;
|
|
32
|
+
build: number;
|
|
33
|
+
/** INFO version: `<version>-<build, 4 digits>`. */
|
|
34
|
+
spkVersion: string;
|
|
35
|
+
displayname: string;
|
|
36
|
+
description: string;
|
|
37
|
+
maintainer: string;
|
|
38
|
+
os_min_ver: string;
|
|
39
|
+
arches: NativeArchFamily[];
|
|
40
|
+
backend: StandaloneBackend;
|
|
41
|
+
ui: {
|
|
42
|
+
dir: string;
|
|
43
|
+
build?: {
|
|
44
|
+
cwd: string;
|
|
45
|
+
command: string[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
i18n?: StandaloneI18n;
|
|
49
|
+
/** `none`: the package account only. `root`: adds DSM-approved root tasks. */
|
|
50
|
+
privilege: "none" | "root";
|
|
51
|
+
/** `root/<name>.sh`, present exactly when privilege is root. */
|
|
52
|
+
rootOps?: RootOp[];
|
|
53
|
+
integration?: Integration;
|
|
54
|
+
}
|
|
55
|
+
export interface StandaloneError {
|
|
56
|
+
code: string;
|
|
57
|
+
message: string;
|
|
58
|
+
}
|
|
59
|
+
export type ManifestResult = {
|
|
60
|
+
ok: true;
|
|
61
|
+
manifest: StandaloneManifest;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
errors: StandaloneError[];
|
|
65
|
+
};
|
|
66
|
+
export declare function loadStandaloneManifest(root: string): ManifestResult;
|
|
67
|
+
/**
|
|
68
|
+
* The nearest directory at or above `start` that holds dsm-package.yaml,
|
|
69
|
+
* unless a Managed Profile project (synology-app.yaml) is found first.
|
|
70
|
+
*/
|
|
71
|
+
export declare function findStandaloneRoot(start: string): string | null;
|