@synopackageland/cli 0.41.3 → 0.41.5
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 +13 -1
- package/README.md +2 -2
- package/dist/build-spk/compose-diagnosis.js +1 -1
- package/dist/build-spk/share-validation.d.ts +3 -0
- package/dist/build-spk/share-validation.js +5 -0
- package/dist/build-spk/wizard-messages.d.ts +99 -0
- package/dist/build-spk/wizard-messages.js +108 -9
- package/dist/cli.js +1 -1
- package/dist/deploy/native-install-values.js +16 -4
- package/dist/error-registry.js +2 -0
- package/dist/init.d.ts +1 -1
- package/dist/init.js +1 -0
- package/dist/runtime/webapi-profile.js +1 -1
- package/dist/standalone/compose.d.ts +4 -0
- package/dist/standalone/compose.js +2 -1
- package/dist/standalone/daemon.js +2 -1
- package/dist/standalone/dsm-window.js +1 -2
- package/dist/standalone/inspect-spk.js +41 -17
- package/dist/standalone/install-ports.d.ts +31 -0
- package/dist/standalone/install-ports.js +103 -0
- package/dist/standalone/install-settings.d.ts +3 -1
- package/dist/standalone/install-settings.js +12 -4
- package/dist/standalone/lifecycle.d.ts +1 -1
- package/dist/standalone/lifecycle.js +3 -2
- package/dist/standalone/manifest.d.ts +1 -0
- package/dist/standalone/manifest.js +16 -0
- package/dist/standalone/native-gateway.d.ts +1 -0
- package/dist/standalone/native-gateway.js +33 -1
- package/dist/standalone/resource.d.ts +1 -0
- package/dist/standalone/resource.js +1 -0
- package/dist/standalone/shared-folder.d.ts +7 -7
- package/dist/standalone/shared-folder.js +75 -16
- package/dist/standalone/spk.js +15 -4
- package/package.json +2 -2
- package/skills/SKILL.md +5 -3
- package/skills/reference/compose.md +18 -14
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +1 -1
- package/skills/reference/identity-gateway.md +5 -1
- package/skills/reference/install.md +41 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { DSM_LANGS, LOCALES } from "./i18n.js";
|
|
2
|
+
import { existingShareValidator } from "../build-spk/share-validation.js";
|
|
1
3
|
import { sharedFolderCombobox } from "../build-spk/share-combobox.js";
|
|
2
|
-
import { renderWizardFiles } from "../build-spk/wizard-i18n.js";
|
|
4
|
+
import { renderWizardFiles, translator } from "../build-spk/wizard-i18n.js";
|
|
3
5
|
import { integrationWizardSteps } from "../build-spk/install-wizard.js";
|
|
4
6
|
const FIELDS = new Set(["mode", "key", "name", "permission"]);
|
|
5
7
|
const KEY = /^[a-z][a-z0-9_]{0,31}$/;
|
|
@@ -14,18 +16,18 @@ export function readSharedFolder(value, fail) {
|
|
|
14
16
|
fail({ code: "STANDALONE_SHARED_FOLDER_INVALID", message: `install.sharedFolder ${why}` });
|
|
15
17
|
return undefined;
|
|
16
18
|
};
|
|
17
|
-
if (!isRecord(value) || Object.keys(value).some((key) => key !== "sharedFolder" && key !== "settings")) {
|
|
19
|
+
if (!isRecord(value) || Object.keys(value).some((key) => key !== "sharedFolder" && key !== "settings" && key !== "ports")) {
|
|
18
20
|
return refuse(": only sharedFolder is allowed.");
|
|
19
21
|
}
|
|
20
|
-
if (value.sharedFolder === undefined && value.settings !== undefined)
|
|
22
|
+
if (value.sharedFolder === undefined && (value.settings !== undefined || value.ports !== undefined))
|
|
21
23
|
return undefined;
|
|
22
24
|
const folder = value.sharedFolder;
|
|
23
25
|
if (!isRecord(folder) || Object.keys(folder).some((key) => !FIELDS.has(key))) {
|
|
24
26
|
return refuse(" accepts only mode, key, name and permission.");
|
|
25
27
|
}
|
|
26
28
|
const { mode, key, name, permission } = folder;
|
|
27
|
-
if (mode !== "select" && mode !== "create")
|
|
28
|
-
return refuse(" mode must be select or create.");
|
|
29
|
+
if (mode !== "select" && mode !== "create" && mode !== "dedicated")
|
|
30
|
+
return refuse(" mode must be dedicated, select or create.");
|
|
29
31
|
if (permission !== "ro" && permission !== "rw")
|
|
30
32
|
return refuse(" permission must be ro or rw.");
|
|
31
33
|
if (typeof key !== "string" || !KEY.test(key) || RESERVED_KEY.test(key)) {
|
|
@@ -51,10 +53,9 @@ export function renderSharedFolderScript(pkg, folder) {
|
|
|
51
53
|
const pick = folder.mode === "create"
|
|
52
54
|
? `name='${folder.name}'`
|
|
53
55
|
: `name=""
|
|
54
|
-
if [ "\${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ] &&
|
|
55
|
-
exit 0
|
|
56
|
-
|
|
57
|
-
name="\${${key}:-}"
|
|
56
|
+
${folder.mode === "dedicated" ? `if [ "\${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ] && [ "\${${key}+x}" != x ]; then exit 0; fi` : ""}
|
|
57
|
+
${folder.mode === "select" ? `if [ "\${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ] && ! change_enabled "\${CHANGE_${key}:-}"; then\n exit 0\nfi` : ""}
|
|
58
|
+
name="\${${key}:-${folder.mode === 'dedicated' ? folder.name : ''}}"
|
|
58
59
|
case "$name" in
|
|
59
60
|
''|*/*|*..*|@*|.*|*[[:cntrl:]]*|*'"'*|*'\\'*) echo "invalid shared folder name for ${key}" >&2; exit 1 ;;
|
|
60
61
|
esac`;
|
|
@@ -79,14 +80,26 @@ exit 0
|
|
|
79
80
|
function intro(t, pkg, folder) {
|
|
80
81
|
const permission = t(`nativeShare.permission.${folder.permission}`);
|
|
81
82
|
return {
|
|
82
|
-
desc: folder.mode
|
|
83
|
+
desc: folder.mode !== "select"
|
|
83
84
|
? t("nativeShare.createIntro", { name: folder.name, permission })
|
|
84
85
|
: t("nativeShare.selectIntro", { account: pkg, permission }),
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
/** Install: the share picker (select) or a note (create). */
|
|
88
89
|
function installItems(t, pkg, folder) {
|
|
89
|
-
|
|
90
|
+
if (folder.mode === "create")
|
|
91
|
+
return [intro(t, pkg, folder)];
|
|
92
|
+
const picker = sharedFolderCombobox(t, folder.key);
|
|
93
|
+
const field = picker.subitems[0];
|
|
94
|
+
field.defaultValue = folder.mode === "dedicated" ? folder.name : "";
|
|
95
|
+
if (folder.mode === "dedicated") {
|
|
96
|
+
field.editable = true;
|
|
97
|
+
field.forceSelection = false;
|
|
98
|
+
}
|
|
99
|
+
field.validator = { allowBlank: folder.mode === "dedicated", fn: existingShareValidator(pkg, folder.permission, t("nativeShare.selectionInvalid"), folder.mode === "dedicated", folder.name) };
|
|
100
|
+
if (folder.mode === "dedicated")
|
|
101
|
+
picker.desc = t("nativeShare.optionalExisting", { name: folder.name });
|
|
102
|
+
return [intro(t, pkg, folder), picker];
|
|
90
103
|
}
|
|
91
104
|
/**
|
|
92
105
|
* Upgrade: DSM prefills the first share, so a checked `CHANGE_<key>` box is what
|
|
@@ -95,8 +108,15 @@ function installItems(t, pkg, folder) {
|
|
|
95
108
|
function upgradeItems(t, pkg, folder) {
|
|
96
109
|
if (folder.mode === "create")
|
|
97
110
|
return [intro(t, pkg, folder)];
|
|
111
|
+
if (folder.mode === "dedicated") {
|
|
112
|
+
const items = installItems(t, pkg, folder);
|
|
113
|
+
const field = items[1].subitems[0];
|
|
114
|
+
field.defaultValue = "";
|
|
115
|
+
field.validator = { allowBlank: false, fn: existingShareValidator(pkg, folder.permission, t("nativeShare.selectionInvalid"), false, folder.name) };
|
|
116
|
+
return items;
|
|
117
|
+
}
|
|
98
118
|
const label = t("label.sharedFolder");
|
|
99
|
-
const picker =
|
|
119
|
+
const picker = installItems(t, pkg, folder)[1];
|
|
100
120
|
const [subitem] = picker.subitems;
|
|
101
121
|
const { defaultValue: _unused, ...blankable } = subitem;
|
|
102
122
|
return [
|
|
@@ -106,7 +126,7 @@ function upgradeItems(t, pkg, folder) {
|
|
|
106
126
|
desc: t("upgrade.changeDesc", { label }),
|
|
107
127
|
subitems: [{ key: `CHANGE_${folder.key}`, desc: t("upgrade.changeSub", { label }), defaultValue: false }],
|
|
108
128
|
},
|
|
109
|
-
{ ...picker, desc: t("upgrade.reuseDesc", { desc: String(picker.desc) }), subitems: [{ ...blankable, validator: { allowBlank: true } }] },
|
|
129
|
+
{ ...picker, desc: t("upgrade.reuseDesc", { desc: String(picker.desc) }), subitems: [{ ...blankable, validator: { allowBlank: true, fn: existingShareValidator(pkg, folder.permission, t("nativeShare.selectionInvalid"), true) } }] },
|
|
110
130
|
];
|
|
111
131
|
}
|
|
112
132
|
/** `install_uifile` and `upgrade_uifile`, English plus one file per DSM language. */
|
|
@@ -122,7 +142,7 @@ export function sharedFolderInstallSteps(pkg, folder) {
|
|
|
122
142
|
}
|
|
123
143
|
/** The upgrade wizard files that let the administrator keep or change the shared folder. */
|
|
124
144
|
export function renderSharedFolderUpgradeWizardFiles(pkg, folder) {
|
|
125
|
-
return
|
|
145
|
+
return renderUpgradeWizardFiles(pkg, folder);
|
|
126
146
|
}
|
|
127
147
|
/**
|
|
128
148
|
* The upgrade wizard: the shared folder choice, then the service callbacks an
|
|
@@ -131,14 +151,29 @@ export function renderSharedFolderUpgradeWizardFiles(pkg, folder) {
|
|
|
131
151
|
export function renderUpgradeWizardFiles(pkg, folder, integrations = []) {
|
|
132
152
|
if (!folder && !integrations.length)
|
|
133
153
|
return {};
|
|
134
|
-
|
|
154
|
+
const files = renderWizardFiles("upgrade_uifile", t => [
|
|
135
155
|
...(folder ? [{ step_title: t("upgrade.step"), items: upgradeItems(t, pkg, folder) }] : []),
|
|
136
156
|
...integrationWizardSteps(t, integrations),
|
|
137
157
|
]);
|
|
158
|
+
if (folder?.mode === "dedicated") {
|
|
159
|
+
for (const locale of LOCALES) {
|
|
160
|
+
const t = translator(locale);
|
|
161
|
+
const steps = [{ step_title: t("upgrade.step"), items: upgradeItems(t, pkg, folder) }, ...integrationWizardSteps(t, integrations)];
|
|
162
|
+
const field = steps[0].items[1].subitems[0];
|
|
163
|
+
field.defaultValue = "__SYNOPKGLAND_RETAINED_SHARE__";
|
|
164
|
+
const invalid = JSON.stringify(steps, null, 2).replace('"__SYNOPKGLAND_RETAINED_SHARE__"', '""');
|
|
165
|
+
field.validator = { allowBlank: true, fn: existingShareValidator(pkg, folder.permission, t("nativeShare.selectionInvalid"), true, folder.name) };
|
|
166
|
+
const retained = JSON.stringify(steps, null, 2);
|
|
167
|
+
const fresh = JSON.stringify([{ step_title: t("upgrade.step"), items: installItems(t, pkg, folder) }, ...integrationWizardSteps(t, integrations)], null, 2);
|
|
168
|
+
const name = locale === "en" ? "upgrade_uifile.sh" : `upgrade_uifile_${DSM_LANGS[locale]}.sh`;
|
|
169
|
+
files[name] = dedicatedUpgradeScript(pkg, folder.key, retained, fresh, invalid);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return files;
|
|
138
173
|
}
|
|
139
174
|
/** `conf/resource` `data-share` entry: create mode only; DSM never deletes the share on uninstall. */
|
|
140
175
|
export function sharedFolderResource(pkg, folder) {
|
|
141
|
-
if (folder.mode
|
|
176
|
+
if (folder.mode === "select")
|
|
142
177
|
return undefined;
|
|
143
178
|
return { shares: [{ name: folder.name, permission: { [folder.permission]: [pkg] } }] };
|
|
144
179
|
}
|
|
@@ -154,3 +189,27 @@ export function recoverSharedFolder(postinst) {
|
|
|
154
189
|
return undefined;
|
|
155
190
|
}
|
|
156
191
|
}
|
|
192
|
+
function dedicatedUpgradeScript(pkg, key, retained, fresh, invalid) {
|
|
193
|
+
return `#!/bin/sh
|
|
194
|
+
set -eu
|
|
195
|
+
config="\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}/config.json"
|
|
196
|
+
[ -z "\${SYNOPKG_TEMP_LOGFILE:-}" ] || exec > "$SYNOPKG_TEMP_LOGFILE"
|
|
197
|
+
if [ ! -e "$config" ]; then
|
|
198
|
+
cat <<'SYNOPKGLAND_WIZARD'
|
|
199
|
+
${fresh}
|
|
200
|
+
SYNOPKGLAND_WIZARD
|
|
201
|
+
else
|
|
202
|
+
name=$(sed -n 's/^{{SCHEMA_PREFIX}}${key}{{NAME_PREFIX}}\\([^"\\\\]*\\){{SUFFIX}}$/\\1/p' "$config")
|
|
203
|
+
case "$name" in ''|*/*|*..*|@*|.*|*[[:cntrl:]]*|*'"'*|*'\\'*) name='' ;; esac
|
|
204
|
+
if [ -z "$name" ]; then
|
|
205
|
+
cat <<'SYNOPKGLAND_WIZARD'
|
|
206
|
+
${invalid}
|
|
207
|
+
SYNOPKGLAND_WIZARD
|
|
208
|
+
else
|
|
209
|
+
cat <<'SYNOPKGLAND_WIZARD' | sed "s/__SYNOPKGLAND_RETAINED_SHARE__/$name/"
|
|
210
|
+
${retained}
|
|
211
|
+
SYNOPKGLAND_WIZARD
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
`.replace('{{SCHEMA_PREFIX}}', '{"schema":1,"shared_folders":{"').replace('{{NAME_PREFIX}}', '":"').replace('{{SUFFIX}}', '"}}');
|
|
215
|
+
}
|
package/dist/standalone/spk.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { portDefaults, portInstallSteps, writePortPayload } from "./install-ports.js";
|
|
1
2
|
import { packageDependencies, dependencyInfo } from "./package-dependencies.js";
|
|
2
3
|
import { settingsInstallSteps, settingsConfig } from './install-settings.js';
|
|
3
4
|
import { backupConfig, backupHookProblem, backupRecord, renderBackupScripts, BACKUP_PAYLOAD_DIR, BACKUP_SCRIPTS_DIR, BACKUP_SCRIPT_MODES } from './hyper-backup.js';
|
|
@@ -32,7 +33,7 @@ import { copyAppTree } from "./bridge-app.js";
|
|
|
32
33
|
import { isBridgeBackend } from "./bridge-backend.js";
|
|
33
34
|
import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
34
35
|
import { writeComposePayload } from "./compose.js";
|
|
35
|
-
import { copyDaemonPayload, daemonSdkVersion } from "./daemon.js";
|
|
36
|
+
import { daemonConfig, copyDaemonPayload, daemonSdkVersion } from "./daemon.js";
|
|
36
37
|
import { DAEMON_INGRESS_CONFIG, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
37
38
|
import { gatewayCgiPaths, gatewayExternalAccess, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig, identityGatewayDeclaration, } from "./identity-gateway.js";
|
|
38
39
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS, } from "./background.js";
|
|
@@ -219,13 +220,14 @@ export async function assembleStandaloneSpk(options) {
|
|
|
219
220
|
chmodSync(join(target, "storage-init"), EXEC_MODE);
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
|
-
if (manifest.installSettings) {
|
|
223
|
+
if (manifest.installSettings || manifest.installPorts) {
|
|
223
224
|
if (!options.installSettingsControl)
|
|
224
225
|
throw new StandaloneBuildError('SDK_INSTALL_SETTINGS_BUILD_FAILED', 'SDK settings controller required.');
|
|
225
226
|
const target = join(staging, 'package/install');
|
|
226
227
|
mkdirSync(target, { recursive: true });
|
|
227
228
|
copyExecutable(options.installSettingsControl, join(target, 'control'));
|
|
228
|
-
|
|
229
|
+
if (manifest.installSettings)
|
|
230
|
+
writeFile(join(target, 'settings.json'), JSON.stringify(settingsConfig(manifest), null, 2) + '\n');
|
|
229
231
|
}
|
|
230
232
|
const backup = options.appBackupControl ? manifest.hyperBackup : undefined;
|
|
231
233
|
if (backup) {
|
|
@@ -235,6 +237,14 @@ export async function assembleStandaloneSpk(options) {
|
|
|
235
237
|
copyExecutable(options.appBackupControl, join(staging, "package", BACKUP_PAYLOAD_DIR, "control"));
|
|
236
238
|
writeFile(join(staging, "package", BACKUP_PAYLOAD_DIR, "config.json"), backupConfig(manifest.package, backup));
|
|
237
239
|
}
|
|
240
|
+
if (manifest.installPorts)
|
|
241
|
+
writePortPayload(manifest, join(staging, 'package'), variant => ({
|
|
242
|
+
'sso/gateway.json': identityGatewayConfig(variant),
|
|
243
|
+
...(nativeGatewaySupported(variant) ? { [NATIVE_GATEWAY_CONFIG]: renderNativeGatewayConfig(variant) } : {}),
|
|
244
|
+
...(hasSdkWindow(variant) ? { ['ui/' + windowScriptName(variant)]: renderWindowScript(variant, assetVersion, icons, options.portalText, moduleVersion) } : {}),
|
|
245
|
+
...(variant.daemon || variant.compose ? { 'daemon/config.json': daemonConfig(variant) } : {}),
|
|
246
|
+
...(variant.installSettings ? { 'install/settings.json': JSON.stringify(settingsConfig(variant), null, 2) + '\n' } : {}),
|
|
247
|
+
}));
|
|
238
248
|
const packageTgz = join(staging, "package.tgz");
|
|
239
249
|
await createReproducibleTar(join(staging, "package"), packageTgz, {
|
|
240
250
|
gzip: true,
|
|
@@ -248,6 +258,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
248
258
|
dependencies: packageDependencies(manifest),
|
|
249
259
|
...(moduleVersion ? { ui_module: moduleVersion } : {}),
|
|
250
260
|
...(manifest.installSettings ? { installSettings: manifest.installSettings } : {}),
|
|
261
|
+
...(manifest.installPorts ? { installPorts: portDefaults(manifest) } : {}),
|
|
251
262
|
...(backup ? { hyperBackup: backupRecord(backup) } : {}),
|
|
252
263
|
...(manifest.integrations ? { integrations: manifest.integrations } : {}),
|
|
253
264
|
...(manifest.lifecycleTimeouts ? { lifecycleTimeouts: manifest.lifecycleTimeouts } : {}),
|
|
@@ -313,7 +324,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
313
324
|
: folder
|
|
314
325
|
? sharedFolderInstallSteps(manifest.package, folder)
|
|
315
326
|
: renderNativeInstallWizard;
|
|
316
|
-
const install = renderInstallWizardFiles(manifest.package, (t) => [...steps(t), ...settingsInstallSteps(manifest.installSettings)(t), ...(portal ? [renderPortalWizardStep(t, portal.alias)] : []), ...integrationWizardSteps(t, manifest.integrations)]);
|
|
327
|
+
const install = renderInstallWizardFiles(manifest.package, (t) => [...steps(t), ...portInstallSteps(manifest)(t), ...settingsInstallSteps(manifest.installSettings, manifest.identityGateway?.listen.https, manifest.installPorts ? manifest.package : undefined)(t), ...(portal ? [renderPortalWizardStep(t, portal.alias)] : []), ...integrationWizardSteps(t, manifest.integrations)]);
|
|
317
328
|
writeWizardUiFiles(staging, {
|
|
318
329
|
...install.static,
|
|
319
330
|
...install.dynamic,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.5",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@napi-rs/keyring": "^2.1.1",
|
|
31
|
-
"@synopackageland/templates": "~0.35.
|
|
31
|
+
"@synopackageland/templates": "~0.35.3",
|
|
32
32
|
"ajv": "^8.17.1",
|
|
33
33
|
"commander": "^13.1.0",
|
|
34
34
|
"json-canonicalize": "^3.0.1",
|
package/skills/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
|
|
|
5
5
|
|
|
6
6
|
# Independent Synology SPKs
|
|
7
7
|
|
|
8
|
-
The CLI installs templates through `~0.35.
|
|
8
|
+
The CLI installs templates through `~0.35.3`. A templates patch needs only a
|
|
9
9
|
templates release; npm install/update selects a patch in that minor line. The CLI
|
|
10
10
|
uses the locally installed templates and reads their SDK pin for defaults; it
|
|
11
11
|
never downloads templates at runtime. Preserve the lockfile and use `npm ci` for
|
|
@@ -16,6 +16,8 @@ The CLI verifies bytes before packaging and never fetches a manifest at NAS runt
|
|
|
16
16
|
Unsupported formats require a CLI update; invalid manifests fail closed. Existing
|
|
17
17
|
SDK 0.26.0/0.27.0 use the frozen legacy inventory.
|
|
18
18
|
|
|
19
|
+
For Apps with data folders or gateway listeners, read [reference/install.md](reference/install.md). Default to an App-named dedicated DSM data share and installer-selected ports. Existing-folder access, port availability and required HTTPS origins must pass localized wizard validation before Next; upgrades retain port selections.
|
|
20
|
+
|
|
19
21
|
All CLI NAS requests share one connection path. NAS certificates are accepted by default, including self-signed, wrong-name and expired certificates. A changed certificate warns once, updates its displayed SHA-256 and continues. `host add --strict-tls` stores strict verification; `--strict-tls` or `--ca <file>` on a command opts in for that command. Read [reference/host-install.md](reference/host-install.md) for host trust and error codes.
|
|
20
22
|
|
|
21
23
|
Before releasing an executor App, read [reference/approval-upgrades.md](reference/approval-upgrades.md). Run build/inspect with the previous released SPK as `--baseline` and include the approval difference in release notes. Without a comparable baseline, retention is unknown.
|
|
@@ -24,7 +26,7 @@ Use this guide to initialize, build and inspect own applications or upstream OSS
|
|
|
24
26
|
|
|
25
27
|
For an existing open-source App, first read [reference/upstream-fork.md](reference/upstream-fork.md): upstream fork/history + untouched upstream files + all packaging in `synology/`, official container images pinned by tag/index digest, native official release binaries pinned by SHA-256 or a source build hook, and a single tag-merge upgrade command. Use `init --upstream-fork`; never rewrite the App as a component. Integrated upstream Apps require direct DSM administrator entry without a separate App password or an OIDC/SSO server: supported trusted headers first, disabling native login second; the shipped OpenList pattern uses the existing SDK 0.17.3 token adapter (`mode: token`, `access: dsm-admins`, `entry: admin-only`) with server-side Authorization injection. All DSM admins share one upstream admin; token reset requires package restart; WebDAV retains its native credentials.
|
|
26
28
|
|
|
27
|
-
Use SDK/App 0.38.
|
|
29
|
+
Use SDK/App 0.38.2 and CLI 0.41.5 for App entry enforcement. Declare every App method
|
|
28
30
|
in the permissions table; dynamic names and unregistered HTTP dispatchers are rejected.
|
|
29
31
|
Use fixed SDK routes and `identity_gateway.upstream_auth_paths` for client API
|
|
30
32
|
paths that rely on upstream authentication. Read
|
|
@@ -41,7 +43,7 @@ cannot upgrade a connection; use an existing upgrade-capable deployment
|
|
|
41
43
|
transport. The audit follows each configured Go binary's Linux import graph,
|
|
42
44
|
including declared workers; unshipped dev commands are excluded.
|
|
43
45
|
|
|
44
|
-
SDK/App 0.38.
|
|
46
|
+
SDK/App 0.38.2 allows 300 seconds for requests by default, with the browser
|
|
45
47
|
waiting 302 seconds. Explicit budgets and earlier parent deadlines still win.
|
|
46
48
|
For long work, use existing independent workers with prompt job admission,
|
|
47
49
|
progress and authorized status/result polling by the original ID. A timed-out
|
|
@@ -15,6 +15,9 @@ maintainer: Example
|
|
|
15
15
|
os_min_ver: "7.2.1-69057"
|
|
16
16
|
arches: [x86_64, aarch64]
|
|
17
17
|
capabilities: {}
|
|
18
|
+
install:
|
|
19
|
+
ports: true
|
|
20
|
+
sharedFolder: {mode: dedicated, key: data, name: MediaApp, permission: rw}
|
|
18
21
|
compose:
|
|
19
22
|
files: [compose.yaml, site.yaml]
|
|
20
23
|
env_files: [.env]
|
|
@@ -23,22 +26,22 @@ compose:
|
|
|
23
26
|
network: {egress: isolated}
|
|
24
27
|
# Private package binds are rejected by DSM build; keep compatible upstream named volumes.
|
|
25
28
|
identity_gateway:
|
|
26
|
-
sdk: 0.
|
|
29
|
+
sdk: 0.38.2
|
|
27
30
|
mode: none
|
|
28
31
|
access: dsm-users
|
|
29
32
|
listen: {http: 28000}
|
|
30
33
|
ui: {launch: new-page}
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
No backend, dummy CGI, UI directory or helper payload is required. Omit `web
|
|
34
|
-
and `
|
|
36
|
+
No backend, dummy CGI, UI directory or helper payload is required. Omit `web`,
|
|
37
|
+
`identity_gateway` and `install.ports` for headless projects. Optional `compose.helpers` names
|
|
35
38
|
architecture-specific native payloads for token/account adapters. All lifecycle
|
|
36
39
|
scripts run as the package account; containers are managed by Container Manager.
|
|
37
40
|
|
|
38
41
|
For external access, pin both `compose.sdk` and `identity_gateway.sdk` to
|
|
39
|
-
0.
|
|
42
|
+
0.38.2 and use the gateway's
|
|
40
43
|
[`external_access` policy and DSM settings entry](identity-gateway.md#external-access).
|
|
41
|
-
|
|
44
|
+
Declare required HTTPS origins through `install.settings`; wizard validation checks the selected HTTPS port before Next. See [install.md](install.md).
|
|
42
45
|
Manual URL configuration does not require Connect. The SDK resolves the public
|
|
43
46
|
origin for Compose; restart the App after changing an origin used by its
|
|
44
47
|
environment or a generated private snapshot.
|
|
@@ -137,12 +140,12 @@ modification and deletion. DSM identity/account bridge and file authority are
|
|
|
137
140
|
separate features. Per-DSM-user filesystem access requires a File Station session
|
|
138
141
|
or Broker Adapter; it is outside this change and cannot be obtained through a mount.
|
|
139
142
|
|
|
140
|
-
For
|
|
143
|
+
For a data Shared Folder, default to the native dedicated share; the wizard also allows a validated existing share:
|
|
141
144
|
|
|
142
145
|
```yaml
|
|
143
146
|
capabilities: {composeMounts: true}
|
|
144
147
|
install:
|
|
145
|
-
sharedFolder: {mode:
|
|
148
|
+
sharedFolder: {mode: dedicated, key: media, name: MediaApp, permission: ro}
|
|
146
149
|
compose:
|
|
147
150
|
shares:
|
|
148
151
|
media:
|
|
@@ -152,19 +155,20 @@ compose:
|
|
|
152
155
|
```
|
|
153
156
|
|
|
154
157
|
The share key and permission must match `install.sharedFolder` exactly. This
|
|
155
|
-
route supports `mode: select`; `create` resource ordering is not supported.
|
|
158
|
+
route supports `mode: dedicated` and `mode: select`; `create` resource ordering is not supported.
|
|
156
159
|
The package-account preinst resolves the chosen name on `/volumeN` and writes
|
|
157
160
|
only the fixed source placeholder into the worker's private `.env`. Missing,
|
|
158
161
|
ambiguous or symlinked sources fail installation. Host paths are never wizard
|
|
159
162
|
answers. Every mounted service uses the fixed package UID:GID, drops all Linux
|
|
160
163
|
capabilities and has no-new-privileges. Container-root cannot mount Shares.
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
DSM creates the dedicated App-named share and grants exactly the declared RO/RW
|
|
166
|
+
through `data-share`; the installer accepts its default without a folder choice.
|
|
167
|
+
An existing share must pass the localized field check for supported folder type
|
|
168
|
+
and package-account access before Next. Unknown access is refused. The SDK
|
|
169
|
+
checks kernel access before startup and reports the share, required permission
|
|
170
|
+
and recovery action if access has been revoked. No App root process or manual
|
|
171
|
+
ACL writer is used. See [install.md](install.md).
|
|
168
172
|
|
|
169
173
|
The upgrade wizard keeps the saved name unless its change checkbox is selected.
|
|
170
174
|
CLI deployment uses `--param media=Media` on first install, and
|
|
@@ -9,7 +9,7 @@ NAS HTTPS accepts any certificate by default. A changed leaf warns and updates t
|
|
|
9
9
|
3. Review `synopkgland deploy --dry-run --arch x86_64`, which does not connect to a NAS.
|
|
10
10
|
4. Only when installation is authorized, run `synopkgland deploy --host <profile>`.
|
|
11
11
|
|
|
12
|
-
Deployment uploads directly to DSM. It does not publish artifacts. Compose uses schema-2 differences and fixed package identity. For `compose.shares` with `select: install`,
|
|
12
|
+
Deployment uploads directly to DSM. It does not publish artifacts. Compose uses schema-2 differences and fixed package identity. For `compose.shares` with `select: install`, default to matching `install.sharedFolder: {mode: dedicated, name: MyApp, key: media, permission: ro}`. DSM creates and grants the dedicated share; existing selections must pass wizard validation before Next. Upgrade keeps the selection unless `--param CHANGE_media=true --param media=OtherMedia` is provided. Use `install.ports: true` for configurable gateway listeners and `--param pkgwizard_port_gateway_https=29443` for first-install automation; upgrade keeps the retained ports, and changing ports uses reinstall. See [install.md](install.md). Dynamic admin-after-install protected ACL/project control remains blocked. A user destination grant never activates a package mount. See compose.md and its device acceptance blockers.
|
|
13
13
|
|
|
14
14
|
For native-go root-ops, approve fixed operations in DSM after installation. Updates that change the task text require fresh approval. Uninstall removes the executor tasks, the App's Application Privileges rules and a stale portal alias itself. Never delete installed packages or data as part of a code cleanup.
|
|
15
15
|
|
|
@@ -87,7 +87,7 @@ sweep forward the cancel instead of admitting it.
|
|
|
87
87
|
|
|
88
88
|
容器使用固定 DSM run-as 使用者與掛載,不代表瀏覽器本人 ACL。容器路徑依 Compose/映像的 upstream target,保留 target、ro/rw,只替換 host source;管理員在安裝時選擇 Share(共用資料夾)。不要對既有資料 chown。App Data 使用 package var;共用資料夾與 named volume 分別備份。`posix_all_shares` 僅為部署時快照,不會自動加入新資料夾。
|
|
89
89
|
|
|
90
|
-
`install.sharedFolder`
|
|
90
|
+
`install.sharedFolder` 預設使用 `mode: dedicated` 與 app 名稱,由 DSM data-share 建立並賦予套件帳號宣告的 RO/RW。使用者可選現有資料夾,但權限與類型必須在 wizard 當步驗證,不可等到啟動後才手動修復。這不是逐使用者 session access;詳見 [install.md](install.md)。移除不刪共用資料夾。Delete App Data 不是完整清理證明,須讀回殘留。
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
### Install-selected scope and the destination API
|
|
@@ -233,8 +233,10 @@ App's responsibility.
|
|
|
233
233
|
An upstream with its own OIDC login can opt into a generic browser bridge:
|
|
234
234
|
|
|
235
235
|
```yaml
|
|
236
|
+
install:
|
|
237
|
+
ports: true
|
|
236
238
|
identity_gateway:
|
|
237
|
-
sdk: 0.
|
|
239
|
+
sdk: 0.38.2
|
|
238
240
|
mode: oidc
|
|
239
241
|
access: dsm-admins
|
|
240
242
|
upstream: 8000
|
|
@@ -290,3 +292,5 @@ produce an App session, or unlock encrypted data. HTTPS and App-specific
|
|
|
290
292
|
fragment/query semantics still require deployment acceptance.
|
|
291
293
|
|
|
292
294
|
Single-origin Apps require SDK 0.30.0 or later. Their `manual_origin` may include an explicit HTTPS port (for example, `https://example.ts.net:29443`). The DSM launcher resolves the destination for the provider used to open DSM, probes `/_spl/ping` for up to eight seconds, and offers **Open anyway** if the probe fails. A single-origin App always uses its configured primary origin. Other Apps and the `external-origin` install setting continue to require HTTPS DNS origins on port 443.
|
|
295
|
+
|
|
296
|
+
Configurable gateway ports use `install.ports: true`; read [install.md](install.md) for field validation, retained selections and upstream runtime bindings. Do not repeat the manifest defaults in App configuration.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Native installation settings
|
|
2
|
+
|
|
3
|
+
Use SDK/App 0.38.2, CLI 0.41.5 and templates 0.35.3. Configure installation through the manifest and generated wizard; do not add App ACL writers, root hooks or independent port state.
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
install:
|
|
7
|
+
ports: true
|
|
8
|
+
sharedFolder:
|
|
9
|
+
mode: dedicated
|
|
10
|
+
key: data
|
|
11
|
+
name: MyApp
|
|
12
|
+
permission: rw
|
|
13
|
+
identity_gateway:
|
|
14
|
+
sdk: 0.38.2
|
|
15
|
+
listen: {https: 28443}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For every App needing shared data, name the dedicated share after the package. DSM's `data-share` resource creates it and grants the package account exactly RO or RW. The wizard defaults to that share. An optional existing share must pass folder-type and package-account access validation before Next; unknown access is refused. Runtime access checks fail promptly with the share name, required access and recovery action. Shared data is retained on uninstall.
|
|
19
|
+
|
|
20
|
+
Port fields default to declared gateway HTTP/HTTPS and local OIDC listener ports. Validation refuses noncanonical numbers, values outside 1024–65535, reserved DSM/discovery ports, duplicates and conflicts from DSM's `SYNO.Core.Service.PortInfo` inventory. Missing or malformed inventory blocks Next. The package-account controller repeats the native service conflict check and materializes gateway, nginx, launcher, daemon and `.sc` port-config data from the selected values.
|
|
21
|
+
|
|
22
|
+
Native subprocesses and Compose `.env` receive `SYNOPKGLAND_PORT_GATEWAY_HTTP`, `SYNOPKGLAND_PORT_GATEWAY_HTTPS` and, when declared, `SYNOPKGLAND_PORT_OIDC_ISSUER`. Go Apps can read `installsettings.PortValues(payloadRoot)`. Bind upstream public-port configuration declaratively:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
compose:
|
|
26
|
+
services:
|
|
27
|
+
web:
|
|
28
|
+
storage_init:
|
|
29
|
+
directories: [{base: /data, path: app}]
|
|
30
|
+
runtime:
|
|
31
|
+
environment:
|
|
32
|
+
PUBLIC_PORT: {port: gateway_https}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Existing runtime file templates can use the same variable in the App's own configuration. Container-internal listener and fixed discovery ports keep their upstream contract. Do not hard-code the selected host port elsewhere.
|
|
36
|
+
|
|
37
|
+
Upgrade retains the saved ports and ignores submitted replacements. Reinstall to change ports, then re-enter any required HTTPS origin with its new HTTPS port. First-install CLI automation accepts `--param pkgwizard_port_gateway_https=29443`; programmatic upgrade refuses port changes. Omit `install.ports` when no identity gateway is declared.
|
|
38
|
+
|
|
39
|
+
Use `install.settings` with `type: https-origin`, a localized label and explicit `required: true` for an origin needed by the App. The wizard blocks Next for missing or malformed values and checks the selected HTTPS port. Runtime validation repeats the check.
|
|
40
|
+
|
|
41
|
+
DSM references: [data-share](https://help.synology.com/developer-guide/resource_acquisition/data_share.html), [service ports](https://help.synology.com/developer-guide/integrate_dsm/ports.html), [port-config](https://help.synology.com/developer-guide/resource_acquisition/port_config.html).
|