@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,205 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, relative, isAbsolute } from "node:path";
|
|
5
|
+
import { infoArchValue } from "../build-spk/dsm-arch.js";
|
|
6
|
+
import { writeDsmWindowIcons, writePackageCenterIcons } from "../build-spk/dsm-icons.js";
|
|
7
|
+
import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
8
|
+
import { createReproducibleTar } from "../build-spk/reproducible-archive.js";
|
|
9
|
+
import { DSM_LANGS, LOCALES } from "./i18n.js";
|
|
10
|
+
import { CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
|
|
11
|
+
const FILE_MODE = 0o644;
|
|
12
|
+
const EXEC_MODE = 0o755;
|
|
13
|
+
const MAX_UI_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
export const LIFECYCLE_SCRIPTS = [
|
|
15
|
+
"preinst",
|
|
16
|
+
"postinst",
|
|
17
|
+
"preuninst",
|
|
18
|
+
"postuninst",
|
|
19
|
+
"preupgrade",
|
|
20
|
+
"postupgrade",
|
|
21
|
+
"start-stop-status",
|
|
22
|
+
];
|
|
23
|
+
export class StandaloneBuildError extends Error {
|
|
24
|
+
code;
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(`[${code}] ${message}`);
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Packs an unsigned Standalone Package SPK. Nothing in it runs as root: the
|
|
32
|
+
* package account owns the payload, lifecycle scripts do nothing, and the
|
|
33
|
+
* only entry point is the CGI DSM runs behind its own login.
|
|
34
|
+
*/
|
|
35
|
+
export async function assembleStandaloneSpk(options) {
|
|
36
|
+
const { manifest } = options;
|
|
37
|
+
const staging = mkdtempSync(join(tmpdir(), "syno-standalone-spk-"));
|
|
38
|
+
try {
|
|
39
|
+
const uiRoot = join(staging, "package", "ui");
|
|
40
|
+
const appDir = join(uiRoot, "app");
|
|
41
|
+
copyUiTree(manifest.ui.dir, appDir);
|
|
42
|
+
const assetVersion = digestTree(appDir).slice(0, 12);
|
|
43
|
+
writeFile(join(uiRoot, "config"), renderUiConfig(manifest));
|
|
44
|
+
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion));
|
|
45
|
+
writeDsmWindowIcons(uiRoot);
|
|
46
|
+
const cgiTarget = join(staging, "package", CGI_PAYLOAD_PATH);
|
|
47
|
+
mkdirSync(dirname(cgiTarget), { recursive: true });
|
|
48
|
+
copyFileSync(options.cgiPath, cgiTarget);
|
|
49
|
+
chmodSync(cgiTarget, EXEC_MODE);
|
|
50
|
+
if (manifest.integration && !options.helper)
|
|
51
|
+
throw new StandaloneBuildError("HELPER_ARTIFACT_REQUIRED", "integration SPK 缺少已建置的 helper inventory。");
|
|
52
|
+
if (options.helper) {
|
|
53
|
+
const dir = join(staging, "package", "integration");
|
|
54
|
+
mkdirSync(dir, { recursive: true });
|
|
55
|
+
copyFileSync(options.helper.binary, join(dir, "helper"));
|
|
56
|
+
chmodSync(join(dir, "helper"), EXEC_MODE);
|
|
57
|
+
writeFile(join(dir, "inventory.json"), JSON.stringify(options.helper.inventory));
|
|
58
|
+
writeFile(join(dir, "workers.json"), JSON.stringify(options.helper.workers.map(({ id, digest }) => ({ id, digest }))));
|
|
59
|
+
mkdirSync(join(dir, "workers"));
|
|
60
|
+
for (const worker of options.helper.workers) {
|
|
61
|
+
writeFileSync(join(dir, "workers", worker.id), worker.bytes, { mode: FILE_MODE });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (manifest.integration?.identity === "better-auth") {
|
|
65
|
+
const source = join(manifest.root, "node", "dist", "app.mjs");
|
|
66
|
+
const rel = relative(realpathSync(manifest.root), realpathSync(source));
|
|
67
|
+
if (rel.startsWith("..") || isAbsolute(rel) || !lstatSync(source).isFile() || lstatSync(source).size > 16 * 1024 * 1024)
|
|
68
|
+
throw new StandaloneBuildError("STANDALONE_IDENTITY_PAYLOAD_INVALID", "Better Auth bundle must be a bounded regular file inside the project.");
|
|
69
|
+
const target = join(staging, "package", "integration", "identity", "app.mjs");
|
|
70
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
71
|
+
copyFileSync(source, target);
|
|
72
|
+
chmodSync(target, FILE_MODE);
|
|
73
|
+
}
|
|
74
|
+
const packageTgz = join(staging, "package.tgz");
|
|
75
|
+
await createReproducibleTar(join(staging, "package"), packageTgz, { gzip: true });
|
|
76
|
+
rmSync(join(staging, "package"), { recursive: true, force: true });
|
|
77
|
+
const checksum = createHash("md5").update(readFileSync(packageTgz)).digest("hex");
|
|
78
|
+
writeFile(join(staging, "INFO"), renderInfo(manifest, options.arch, checksum));
|
|
79
|
+
mkdirSync(join(staging, "conf"));
|
|
80
|
+
writeFile(join(staging, "conf", "privilege"), renderNativePrivilege());
|
|
81
|
+
mkdirSync(join(staging, "scripts"));
|
|
82
|
+
for (const name of LIFECYCLE_SCRIPTS) {
|
|
83
|
+
writeFile(join(staging, "scripts", name), renderScript(name, manifest), EXEC_MODE);
|
|
84
|
+
}
|
|
85
|
+
writePackageCenterIcons(staging);
|
|
86
|
+
mkdirSync(dirname(options.outputPath), { recursive: true });
|
|
87
|
+
await createReproducibleTar(staging, options.outputPath);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
rmSync(staging, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function writeFile(path, content, mode = FILE_MODE) {
|
|
94
|
+
writeFileSync(path, content, "utf8");
|
|
95
|
+
chmodSync(path, mode);
|
|
96
|
+
}
|
|
97
|
+
function quote(value) {
|
|
98
|
+
return `"${value}"`;
|
|
99
|
+
}
|
|
100
|
+
/** Manifest validation already refused quotes, backslashes and newlines. */
|
|
101
|
+
function renderInfo(manifest, arch, checksum) {
|
|
102
|
+
const fields = [
|
|
103
|
+
["package", manifest.package],
|
|
104
|
+
["version", manifest.spkVersion],
|
|
105
|
+
["displayname", manifest.displayname],
|
|
106
|
+
["description", manifest.description],
|
|
107
|
+
["maintainer", manifest.maintainer],
|
|
108
|
+
["arch", infoArchValue(arch)],
|
|
109
|
+
["os_min_ver", manifest.os_min_ver],
|
|
110
|
+
["dsmuidir", "ui"],
|
|
111
|
+
["dsmappname", `SYNO.SDS.${manifest.package}.Instance`],
|
|
112
|
+
["thirdparty", "yes"],
|
|
113
|
+
["startable", "yes"],
|
|
114
|
+
["checksum", checksum],
|
|
115
|
+
];
|
|
116
|
+
if (manifest.integration?.identity === "better-auth")
|
|
117
|
+
fields.push(["install_dep_packages", "Node.js_v22"]);
|
|
118
|
+
for (const locale of LOCALES) {
|
|
119
|
+
const text = manifest.i18n?.info[locale];
|
|
120
|
+
if (text?.displayname)
|
|
121
|
+
fields.push([`displayname_${DSM_LANGS[locale]}`, text.displayname]);
|
|
122
|
+
if (text?.description)
|
|
123
|
+
fields.push([`description_${DSM_LANGS[locale]}`, text.description]);
|
|
124
|
+
}
|
|
125
|
+
return fields.map(([key, value]) => `${key}=${quote(value)}\n`).join("");
|
|
126
|
+
}
|
|
127
|
+
/** No lifecycle work: the CGI needs nothing installed, started or cleaned up. */
|
|
128
|
+
function renderScript(name, manifest) {
|
|
129
|
+
if (manifest.integration && ["preupgrade", "preuninst", "start-stop-status"].includes(name)) {
|
|
130
|
+
const check = `/var/packages/${manifest.package}/target/integration/helper check-stopped || {
|
|
131
|
+
echo 'Run the approved helper_teardown or helper_disable operation before stopping, upgrading or uninstalling.' >&2
|
|
132
|
+
exit 1
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
if (name !== "start-stop-status")
|
|
136
|
+
return `#!/bin/sh\n${check}`;
|
|
137
|
+
return `#!/bin/sh
|
|
138
|
+
case "$1" in
|
|
139
|
+
start|status) exit 0 ;;
|
|
140
|
+
stop)
|
|
141
|
+
${check} ;;
|
|
142
|
+
*) exit 1 ;;
|
|
143
|
+
esac
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
if (name === "start-stop-status") {
|
|
147
|
+
return `#!/bin/sh
|
|
148
|
+
case "$1" in
|
|
149
|
+
start|stop|status) exit 0 ;;
|
|
150
|
+
*) exit 1 ;;
|
|
151
|
+
esac
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
return "#!/bin/sh\nexit 0\n";
|
|
155
|
+
}
|
|
156
|
+
function copyUiTree(source, target) {
|
|
157
|
+
for (const entry of ["app.js", "app.css"]) {
|
|
158
|
+
if (!existsSync(join(source, entry))) {
|
|
159
|
+
throw new StandaloneBuildError("STANDALONE_UI_ENTRY_MISSING", `UI 目錄缺少 ${entry}:${source}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let total = 0;
|
|
163
|
+
const walk = (from, to) => {
|
|
164
|
+
mkdirSync(to, { recursive: true });
|
|
165
|
+
chmodSync(to, EXEC_MODE);
|
|
166
|
+
for (const name of readdirSync(from).sort()) {
|
|
167
|
+
const src = join(from, name);
|
|
168
|
+
const stat = lstatSync(src);
|
|
169
|
+
if (stat.isSymbolicLink()) {
|
|
170
|
+
throw new StandaloneBuildError("STANDALONE_UI_SYMLINK", `UI 目錄不可含 symlink:${src}`);
|
|
171
|
+
}
|
|
172
|
+
if (stat.isDirectory()) {
|
|
173
|
+
walk(src, join(to, name));
|
|
174
|
+
}
|
|
175
|
+
else if (stat.isFile()) {
|
|
176
|
+
total += stat.size;
|
|
177
|
+
if (total > MAX_UI_BYTES) {
|
|
178
|
+
throw new StandaloneBuildError("STANDALONE_UI_TOO_LARGE", "UI 目錄超過 64 MiB。");
|
|
179
|
+
}
|
|
180
|
+
copyFileSync(src, join(to, name));
|
|
181
|
+
chmodSync(join(to, name), FILE_MODE);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
throw new StandaloneBuildError("STANDALONE_UI_SPECIAL_FILE", `UI 目錄只可含一般檔案與目錄:${src}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
walk(source, target);
|
|
189
|
+
}
|
|
190
|
+
function digestTree(root) {
|
|
191
|
+
const hash = createHash("sha256");
|
|
192
|
+
const walk = (dir, prefix) => {
|
|
193
|
+
for (const name of readdirSync(dir).sort()) {
|
|
194
|
+
const path = join(dir, name);
|
|
195
|
+
if (lstatSync(path).isDirectory()) {
|
|
196
|
+
walk(path, `${prefix}${name}/`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
hash.update(`${prefix}${name}\0`).update(readFileSync(path));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
walk(root, "");
|
|
204
|
+
return hash.digest("hex");
|
|
205
|
+
}
|
|
@@ -3,7 +3,6 @@ services:
|
|
|
3
3
|
image: alpine:3.20
|
|
4
4
|
command: ["sleep", "infinity"]
|
|
5
5
|
volumes:
|
|
6
|
-
# Dynamic Shared Folder plus writable /state is Broker-managed and is
|
|
7
6
|
# intentionally outside the zero-config docker-project resource-worker path.
|
|
8
7
|
- "${MEDIA_SHARE}/data:/data:ro"
|
|
9
8
|
- "${SYNOPKG_PKGVAR}/state:/state"
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
services:
|
|
2
2
|
web:
|
|
3
3
|
image: busybox:1.36
|
|
4
|
-
#
|
|
5
|
-
# 1) command
|
|
6
|
-
# docker-project worker
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
4
|
+
# DSM worker constraints to preserve when changing this service:
|
|
5
|
+
# 1) command/entrypoint arrays must not contain bare numeric elements
|
|
6
|
+
# (such as "18333"). DSM's docker-project worker drops the quotes on a
|
|
7
|
+
# YAML round-trip and turns them into integers, so postinst acquire
|
|
8
|
+
# fails (error 276 -> package broken/150, with an empty worker_msg that
|
|
9
|
+
# tells the developer nothing). Keep the port inside one string.
|
|
10
|
+
# `synopkgland validate` already rejects the bare form.
|
|
11
|
+
# 2) The default is not 8080: that port is often taken by an existing
|
|
12
|
+
# container on a real NAS, and the conflict shows up as the same
|
|
13
|
+
# unexplained broken/150.
|
|
14
|
+
# The official nginx image cannot be used either: the resource worker
|
|
15
|
+
# injects an arbitrary package UID/GID, and the nginx entrypoint needs
|
|
16
|
+
# root during startup, so it exits immediately under that identity.
|
|
14
17
|
command: ["sh","-c","httpd -f -p 18333 -h /usr/share/nginx/html"]
|
|
15
18
|
ports:
|
|
16
19
|
- "18333:18333"
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const status = document.getElementById("status");
|
|
4
|
-
const userEl = document.getElementById("user");
|
|
5
|
-
|
|
6
|
-
const client = createSynologyClient();
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
await client.auth.requireLogin();
|
|
10
|
-
const user = await client.auth.getCurrentUser();
|
|
11
|
-
if (status) {
|
|
12
|
-
status.textContent = "Signed in with DSM";
|
|
13
|
-
}
|
|
14
|
-
if (userEl) {
|
|
15
|
-
userEl.textContent = user.username;
|
|
16
|
-
}
|
|
17
|
-
} catch (error) {
|
|
18
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
19
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
-
if (status) {
|
|
21
|
-
status.textContent = code === "UNAUTHENTICATED" ? "DSM login required" : message;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
document.getElementById("status").textContent = "Your app is running.";
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
services:
|
|
2
2
|
web:
|
|
3
3
|
image: busybox:1.36
|
|
4
|
-
#
|
|
5
|
-
# 1) command
|
|
6
|
-
# docker-project worker
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
4
|
+
# DSM worker constraints to preserve when changing this service:
|
|
5
|
+
# 1) command/entrypoint arrays must not contain bare numeric elements
|
|
6
|
+
# (such as "18333"). DSM's docker-project worker drops the quotes on a
|
|
7
|
+
# YAML round-trip and turns them into integers, so postinst acquire
|
|
8
|
+
# fails (error 276 -> package broken/150, with an empty worker_msg that
|
|
9
|
+
# tells the developer nothing). Keep the port inside one string.
|
|
10
|
+
# `synopkgland validate` already rejects the bare form.
|
|
11
|
+
# 2) The default is not 8080: that port is often taken by an existing
|
|
12
|
+
# container on a real NAS, and the conflict shows up as the same
|
|
13
|
+
# unexplained broken/150.
|
|
14
|
+
# The official nginx image cannot be used either: the resource worker
|
|
15
|
+
# injects an arbitrary package UID/GID, and the nginx entrypoint needs
|
|
16
|
+
# root during startup, so it exits immediately under that identity.
|
|
14
17
|
command: ["sh","-c","httpd -f -p 18333 -h /usr/share/nginx/html"]
|
|
15
18
|
ports:
|
|
16
19
|
- "18333:18333"
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const status = document.getElementById("status");
|
|
4
|
-
const userEl = document.getElementById("user");
|
|
5
|
-
|
|
6
|
-
const client = createSynologyClient();
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
await client.auth.requireLogin();
|
|
10
|
-
const user = await client.auth.getCurrentUser();
|
|
11
|
-
if (status) {
|
|
12
|
-
status.textContent = "Signed in with DSM";
|
|
13
|
-
}
|
|
14
|
-
if (userEl) {
|
|
15
|
-
userEl.textContent = user.username;
|
|
16
|
-
}
|
|
17
|
-
} catch (error) {
|
|
18
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
19
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
-
if (status) {
|
|
21
|
-
status.textContent = code === "UNAUTHENTICATED" ? "DSM login required" : message;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
document.getElementById("status").textContent = "Your app is running.";
|
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
9
|
<h1>Hello Web Page</h1>
|
|
10
|
-
<p id="status">
|
|
11
|
-
<p id="user"></p>
|
|
12
|
-
<script src="./bootstrap.js"></script>
|
|
10
|
+
<p id="status">Loading…</p>
|
|
13
11
|
<script type="module" src="./app.js"></script>
|
|
14
12
|
</body>
|
|
15
13
|
</html>
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# __DISPLAY_NAME__
|
|
2
|
+
|
|
3
|
+
A native DSM package (`__PACKAGE__`): a Go backend that DSM runs as a CGI,
|
|
4
|
+
and a UI that opens in its own DSM window. It installs as an **unsigned** SPK,
|
|
5
|
+
runs as its own DSM package account, needs no containers, and has no root by default.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
DSM main menu → DSM window (ui/app.js)
|
|
9
|
+
│ ctx.call("greeting.get") same-origin POST + DSM session
|
|
10
|
+
▼
|
|
11
|
+
/webman/3rdparty/__PACKAGE__/cgi/api.cgi (backend/, runs as package user)
|
|
12
|
+
1. transport guards: POST, JSON, same origin, ≤ 1 MiB
|
|
13
|
+
2. one DSM authentication (unless the route is Public)
|
|
14
|
+
3. route policy: User or Admin → refusal before any handler
|
|
15
|
+
4. your handler (backend/internal/app)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
Needs Node.js with `synopkgland` installed, and Go 1.24+.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
(cd backend && go test ./...) # auth policy + route tests
|
|
24
|
+
synopkgland build-spk # → dist/__PACKAGE__-0.1.0-0001-{x86_64,aarch64}.spk
|
|
25
|
+
synopkgland inspect --spk dist/__PACKAGE__-0.1.0-0001-x86_64.spk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Install: Package Center → Settings → General → Trust Level "Any publisher",
|
|
29
|
+
then Manual Install with the SPK for your NAS's CPU. Open
|
|
30
|
+
**__DISPLAY_NAME__** from the DSM main menu: it shows who you are signed in
|
|
31
|
+
as, what the backend reports, and (for administrators) a form that stores a
|
|
32
|
+
greeting in the package's data folder.
|
|
33
|
+
|
|
34
|
+
## Project layout
|
|
35
|
+
|
|
36
|
+
| Path | What it is | Edit it? |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `dsm-package.yaml` | Package identity, version/build, arches, where backend and UI come from | Yes |
|
|
39
|
+
| `ui/app.js`, `ui/app.css` | The app inside the DSM window | Yes — your UI |
|
|
40
|
+
| `ui/…` | Any other assets; served from `/webman/3rdparty/__PACKAGE__/app/` | Yes |
|
|
41
|
+
| `i18n/messages/<locale>.json` | UI text in nine languages | Yes |
|
|
42
|
+
| `i18n/glossary.json` | Synology's official DSM terms, and which messages must use them | Add terms |
|
|
43
|
+
| `backend/internal/app/` | Your API: routes and handlers | Yes — your backend |
|
|
44
|
+
| `backend/internal/api/` | The CGI front door: guards, auth order, route policies | Rarely |
|
|
45
|
+
| `backend/internal/dsm/` | DSM session check and administrators lookup | Rarely |
|
|
46
|
+
| `backend/cmd/app/` | Wires the above together | Rarely |
|
|
47
|
+
| `dist/` | Built SPKs (ignored by git) | No |
|
|
48
|
+
|
|
49
|
+
`build-spk` generates everything else: `INFO`, `conf/privilege`
|
|
50
|
+
(`run-as: package`), no-op lifecycle scripts, icons, the DSM window wrapper
|
|
51
|
+
(`ui/config`, `ui/window-<build>.js`) and the CGI at `ui/cgi/api.cgi`.
|
|
52
|
+
|
|
53
|
+
## Add an API operation
|
|
54
|
+
|
|
55
|
+
Add a route in `backend/internal/app/routes.go`. The policy is part of the
|
|
56
|
+
route, so you cannot register a handler without one:
|
|
57
|
+
|
|
58
|
+
```go
|
|
59
|
+
func Routes(cfg Config) []api.Route {
|
|
60
|
+
return []api.Route{
|
|
61
|
+
api.User("app.status", cfg.status),
|
|
62
|
+
api.User("greeting.get", cfg.getGreeting),
|
|
63
|
+
api.Admin("greeting.set", cfg.setGreeting),
|
|
64
|
+
api.User("notes.list", cfg.listNotes), // new: any signed-in user
|
|
65
|
+
api.Admin("notes.purge", cfg.purgeNotes), // new: administrators only
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func (cfg Config) listNotes(ctx context.Context, caller api.Caller, params json.RawMessage) (any, error) {
|
|
70
|
+
var in struct{ Limit int `json:"limit"` }
|
|
71
|
+
if err := decodeParams(params, &in); err != nil {
|
|
72
|
+
return nil, err
|
|
73
|
+
}
|
|
74
|
+
return map[string]any{"owner": caller.User(), "notes": []string{}}, nil
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then add the method's valid params to `sampleParams` in `routes_test.go`; the
|
|
79
|
+
test fails until you do, and once you do it proves the new route refuses
|
|
80
|
+
callers without a session (and non-admins, for `api.Admin`) without touching
|
|
81
|
+
the data folder.
|
|
82
|
+
|
|
83
|
+
Call it from the UI with `ctx.call("notes.list", { limit: 20 })`. Errors
|
|
84
|
+
reject with `{ code, message }`, for example `session_required`,
|
|
85
|
+
`admin_required`, `bad_request` or `internal`.
|
|
86
|
+
|
|
87
|
+
### Who can call what
|
|
88
|
+
|
|
89
|
+
| Declared with | No session | Signed-in user | Administrator |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `api.User` | `session_required` | runs | runs |
|
|
92
|
+
| `api.Admin` | `session_required` | `admin_required` | runs |
|
|
93
|
+
| `api.Public` | runs | runs | runs |
|
|
94
|
+
| not registered | `session_required` | `unknown_method` | `unknown_method` |
|
|
95
|
+
|
|
96
|
+
Every reply, error or not, is HTTP 200 with `{"success": false, "error":
|
|
97
|
+
{"code", "message"}}` on failure, the same convention as DSM's own WebAPI.
|
|
98
|
+
DSM's web server replaces the body of any CGI reply with status 403, 404 or
|
|
99
|
+
5xx by its own HTML error page, so a status-coded error would never reach your
|
|
100
|
+
UI.
|
|
101
|
+
|
|
102
|
+
- Handlers get an `api.Caller` the server built after authenticating; code
|
|
103
|
+
outside `internal/api` cannot construct an authenticated one.
|
|
104
|
+
- `api.Public` runs with **no** DSM login. Use it only for data you would show
|
|
105
|
+
anyone who can reach DSM's port; the template ships none.
|
|
106
|
+
- "Administrator" means a member of DSM's `administrators` group, read fresh
|
|
107
|
+
from `/etc/group` on every request.
|
|
108
|
+
|
|
109
|
+
## How authentication works (and its limits)
|
|
110
|
+
|
|
111
|
+
The CGI hands only the session carriers (`Cookie`, `X-SYNO-TOKEN`, client
|
|
112
|
+
address) to DSM's own `/usr/syno/synoman/webman/modules/authenticate.cgi`,
|
|
113
|
+
which prints the signed-in user name. The backend never parses the session
|
|
114
|
+
cookie and never takes a user name or admin flag from the request.
|
|
115
|
+
|
|
116
|
+
Cross-site requests are refused: the API accepts only `POST` with
|
|
117
|
+
`Content-Type: application/json` (which forces a CORS preflight DSM never
|
|
118
|
+
approves), and it rejects `Sec-Fetch-Site` other than `same-origin` and any
|
|
119
|
+
foreign `Origin`. The DSM window wrapper sends DSM's `SynoToken`, but when DSM
|
|
120
|
+
has `enable_syno_token=no` that token proves nothing, so the same-origin
|
|
121
|
+
checks are what stop cross-site requests.
|
|
122
|
+
|
|
123
|
+
Known limits:
|
|
124
|
+
|
|
125
|
+
- Each request reuses the browser's DSM session; it does not carry DSM's
|
|
126
|
+
per-request `X-SYNO-HASH` proof. Verifying that proof needs private,
|
|
127
|
+
version-specific DSM libraries through a C bridge, so it is not part of this
|
|
128
|
+
template.
|
|
129
|
+
- Users from a directory service (LDAP/AD) who are administrators only
|
|
130
|
+
through directory groups are not listed in `/etc/group`, so they count as
|
|
131
|
+
ordinary users. This errs towards refusal.
|
|
132
|
+
|
|
133
|
+
## Customize the UI
|
|
134
|
+
|
|
135
|
+
`ui/app.js` must register `window.DsmPackageApps[<package>].mount(root, ctx)`
|
|
136
|
+
(the template shows how, using `document.currentScript.dataset.package`).
|
|
137
|
+
`ctx.call(method, params)` is the only transport. `ctx.reopen(fn)` runs `fn`
|
|
138
|
+
whenever DSM shows the window again. Scope your CSS under your own root class:
|
|
139
|
+
every DSM app shares one page.
|
|
140
|
+
|
|
141
|
+
To build the UI with Vite, Vue or React, point `ui` at the build output and
|
|
142
|
+
give the build command as an argv list (no shell):
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
ui:
|
|
146
|
+
dir: web/dist
|
|
147
|
+
build:
|
|
148
|
+
cwd: web
|
|
149
|
+
command: [npm, run, build]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The build must emit `app.js` (one script that registers `mount`) and
|
|
153
|
+
`app.css`. The public `@synopackageland/ui@0.4.0` package on
|
|
154
|
+
`https://registry.npmjs.org` gives Vue components with DSM 7 styling;
|
|
155
|
+
import its `themes/dsm7-embedded.css` for use inside a DSM window.
|
|
156
|
+
|
|
157
|
+
## Languages
|
|
158
|
+
|
|
159
|
+
The UI ships in nine of DSM's languages: `en`, `zh-TW`,
|
|
160
|
+
`zh-CN`, `ja`, `ko`, `de`, `fr`, `es` and `it`. DSM's other languages show
|
|
161
|
+
English. The window follows DSM's own language setting (`_S("lang")`).
|
|
162
|
+
|
|
163
|
+
- **Messages**: `i18n/messages/<locale>.json`, flat `"key": "text"`. Use
|
|
164
|
+
`ctx.t("greeting.setBy", { text, user })` in the UI; `{name}` is a
|
|
165
|
+
placeholder, and `ctx.t("files", { count: 3 })` picks `files.one` or
|
|
166
|
+
`files.other`. Missing keys fall back to English, then to the key itself.
|
|
167
|
+
`build-spk` embeds the catalogs in the DSM window script, so there is no
|
|
168
|
+
extra request.
|
|
169
|
+
- **Errors**: the backend returns a `code`; the UI shows
|
|
170
|
+
`error.<code>` from the catalogs and uses the backend's English text only for
|
|
171
|
+
codes it has no message for. Add `error.<code>` for every code you return.
|
|
172
|
+
- **Glossary**: `i18n/glossary.json` records DSM's own words in all nine
|
|
173
|
+
languages, each with its source (`section/key` in DSM 7.4's
|
|
174
|
+
`/usr/syno/synoman/webman/texts/<lang>/strings`). A term's `keys` lists the
|
|
175
|
+
messages that must contain it; that is how "Save" uses DSM's own word in
|
|
176
|
+
every language, the same as the rest of DSM. Product names in `untranslated`
|
|
177
|
+
stay in English everywhere.
|
|
178
|
+
- **Package Center text**: `i18n.info` in `dsm-package.yaml` writes INFO
|
|
179
|
+
`displayname_<lang>` / `description_<lang>` (for example `description_cht`).
|
|
180
|
+
|
|
181
|
+
`synopkgland validate` and `build-spk` refuse a language file with missing or
|
|
182
|
+
extra keys, a translation that drops a `{placeholder}`, an unsupported
|
|
183
|
+
language, or a listed message that drifts from the glossary.
|
|
184
|
+
|
|
185
|
+
To add a message: add the key to `en.json` and to the other eight files. To
|
|
186
|
+
use a DSM term, copy its `text` from the glossary and add your key to the
|
|
187
|
+
term's `keys`. A Vue or React UI can keep its own i18n library; `ctx.t` and
|
|
188
|
+
`ctx.locale` are there if you want them.
|
|
189
|
+
|
|
190
|
+
## Package an existing binary
|
|
191
|
+
|
|
192
|
+
Keep the Go backend and call your tool from a handler (recommended: auth
|
|
193
|
+
stays central), or replace the CGI with your own executable:
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
arches: [x86_64]
|
|
197
|
+
backend:
|
|
198
|
+
binary:
|
|
199
|
+
x86_64: prebuilt/api-x86_64
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The file must be a regular file in the project, a 64-bit Linux ELF for that
|
|
203
|
+
arch, and statically linked (DSM models ship different libc versions). It is
|
|
204
|
+
installed as `ui/cgi/api.cgi` (mode 0755) and runs as the package account.
|
|
205
|
+
It then owns everything `backend/internal/api` does for you: bounded reads,
|
|
206
|
+
POST-only JSON, same-origin checks, authentication before any work, and a
|
|
207
|
+
refusal for unknown methods.
|
|
208
|
+
|
|
209
|
+
## Data, upgrades, uninstall
|
|
210
|
+
|
|
211
|
+
The backend stores data in `/var/packages/__PACKAGE__/var` (the package's
|
|
212
|
+
own folder, kept across upgrades). Bump `build` for every SPK you hand out;
|
|
213
|
+
bump `version` for feature releases. There are no lifecycle scripts to write:
|
|
214
|
+
the CGI starts per request, so start/stop do nothing.
|
|
215
|
+
|
|
216
|
+
## Root access
|
|
217
|
+
|
|
218
|
+
By default (`privilege: none`) nothing in this package ever runs as root, and
|
|
219
|
+
that is the right choice for almost every app. Lifecycle scripts and the CGI
|
|
220
|
+
run as the package account, and nothing the package account can write is ever
|
|
221
|
+
run as root.
|
|
222
|
+
|
|
223
|
+
If your app truly needs root for a few fixed actions, create the project with
|
|
224
|
+
`synopkgland init -t native-go --privilege root`. That sets `privilege: root`
|
|
225
|
+
and adds:
|
|
226
|
+
|
|
227
|
+
- `root/whoami.sh`: one root operation. Each `root/<name>.sh` (lowercase name,
|
|
228
|
+
`#!/bin/sh`, at most 16 KiB) is one operation. The demo only reports the
|
|
229
|
+
user ID it runs as (`{"euid":0}`). Replace it with your own fixed actions.
|
|
230
|
+
- `backend/internal/app/root.go`: the `root.output` route (administrators
|
|
231
|
+
only) that returns an operation's last result.
|
|
232
|
+
|
|
233
|
+
How it works, with no keys or signing involved:
|
|
234
|
+
|
|
235
|
+
1. `build-spk` embeds each operation's exact task text in the DSM window. The
|
|
236
|
+
SPK payload carries no root script, and the package still runs as its own
|
|
237
|
+
account.
|
|
238
|
+
2. The window shows an authorization panel above your app, in the user's
|
|
239
|
+
DSM language. A DSM administrator clicks **Authorize** and confirms with
|
|
240
|
+
their DSM password in DSM's own dialog. DSM then creates one **disabled**
|
|
241
|
+
root task in Task Scheduler, named `<package> root: <name>`. The
|
|
242
|
+
confirmation is used for that one task and never stored.
|
|
243
|
+
3. **Run** checks that the task is still exactly the approved text, runs it
|
|
244
|
+
with the administrator's session and shows its result. The task writes
|
|
245
|
+
only to `/run/synopkgland-<package>/<name>.out`, which root owns and your
|
|
246
|
+
backend can only read.
|
|
247
|
+
4. The task text includes the package version, so after an upgrade the panel
|
|
248
|
+
shows "needs authorization again" and the old task never runs. A
|
|
249
|
+
same-named task that the panel did not create is never run, replaced or
|
|
250
|
+
deleted.
|
|
251
|
+
5. **Remove** deletes the task, also an outdated one, without asking for the
|
|
252
|
+
password. Remove it before uninstalling the package, because DSM keeps
|
|
253
|
+
Task Scheduler tasks after an uninstall.
|
|
254
|
+
|
|
255
|
+
Non-administrators see DSM's "Only administrator accounts can perform this
|
|
256
|
+
operation." and nothing else. HTTP requests can never choose what runs as
|
|
257
|
+
root: the only root code is the fixed text an administrator approved. Your app
|
|
258
|
+
can also use `ctx.root` (`state`, `authorize`, `run`, `remove`, `output`) to
|
|
259
|
+
drive the same flow from its own UI.
|
|
260
|
+
|
|
261
|
+
## Support matrix
|
|
262
|
+
|
|
263
|
+
| | Status |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| Build hosts | macOS, Linux (any OS with Go and Node) |
|
|
266
|
+
| Target CPUs | x86_64 and aarch64 DSM models (one SPK each) |
|
|
267
|
+
| DSM | 7.2 or later declared (`os_min_ver`). Installed and tested on DSM 7.4 (x86_64). aarch64 SPKs are built the same way but have not been run on a NAS. |
|
|
268
|
+
| Not supported | 32-bit ARM, DSM 6, running anything as root without an administrator's approval |
|