@synopackageland/cli 0.41.4 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/README.md +2 -2
  3. package/dist/build-spk/compose-diagnosis.js +1 -1
  4. package/dist/build-spk/share-validation.d.ts +3 -0
  5. package/dist/build-spk/share-validation.js +5 -0
  6. package/dist/build-spk/wizard-messages.d.ts +99 -0
  7. package/dist/build-spk/wizard-messages.js +108 -9
  8. package/dist/cli.js +1 -1
  9. package/dist/deploy/native-install-values.js +16 -4
  10. package/dist/error-registry.js +2 -0
  11. package/dist/init.d.ts +1 -1
  12. package/dist/init.js +1 -0
  13. package/dist/standalone/compose.d.ts +4 -0
  14. package/dist/standalone/compose.js +2 -1
  15. package/dist/standalone/daemon.js +2 -1
  16. package/dist/standalone/dsm-window.js +1 -2
  17. package/dist/standalone/inspect-spk.js +41 -17
  18. package/dist/standalone/install-ports.d.ts +31 -0
  19. package/dist/standalone/install-ports.js +103 -0
  20. package/dist/standalone/install-settings.d.ts +3 -1
  21. package/dist/standalone/install-settings.js +12 -4
  22. package/dist/standalone/lifecycle.d.ts +1 -1
  23. package/dist/standalone/lifecycle.js +3 -2
  24. package/dist/standalone/manifest.d.ts +1 -0
  25. package/dist/standalone/manifest.js +16 -0
  26. package/dist/standalone/native-gateway.d.ts +1 -0
  27. package/dist/standalone/native-gateway.js +33 -1
  28. package/dist/standalone/resource.d.ts +1 -0
  29. package/dist/standalone/resource.js +1 -0
  30. package/dist/standalone/shared-folder.d.ts +7 -7
  31. package/dist/standalone/shared-folder.js +75 -16
  32. package/dist/standalone/spk.js +15 -4
  33. package/package.json +2 -2
  34. package/skills/SKILL.md +5 -3
  35. package/skills/reference/compose.md +18 -14
  36. package/skills/reference/deploy.md +1 -1
  37. package/skills/reference/files.md +1 -1
  38. package/skills/reference/identity-gateway.md +5 -1
  39. 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" ] && ! change_enabled "\${CHANGE_${key}:-}"; then
55
- exit 0
56
- fi
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 === "create"
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
- return folder.mode === "create" ? [intro(t, pkg, folder)] : [intro(t, pkg, folder), sharedFolderCombobox(t, folder.key)];
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 = sharedFolderCombobox(t, folder.key);
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 renderWizardFiles("upgrade_uifile", t => [{ step_title: t("upgrade.step"), items: upgradeItems(t, pkg, folder) }]);
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
- return renderWizardFiles("upgrade_uifile", t => [
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 !== "create")
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
+ }
@@ -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
- writeFile(join(target, 'settings.json'), JSON.stringify(settingsConfig(manifest), null, 2) + '\n');
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.4",
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.2",
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.2`. A templates patch needs only a
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.2 and CLI 0.41.4 for App entry enforcement. Declare every App method
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.1 allows 300 seconds for requests by default, with the browser
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.17.3
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 `identity_gateway` for headless projects. Optional `compose.helpers` names
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.29.0 or later and use the gateway's
42
+ 0.38.2 and use the gateway's
40
43
  [`external_access` policy and DSM settings entry](identity-gateway.md#external-access).
41
- There are no installation-wizard URL fields or post-install state initialization.
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 one existing Shared Folder, reuse the native install/upgrade wizard:
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: select, key: media, permission: ro}
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
- **Selecting an existing share does not grant permission.** An administrator must
163
- grant the package account access in **Control Panel > Shared Folder > Edit >
164
- Permissions > System internal user**. Until then, container file operations fail
165
- under the kernel's ACL checks. No root helper, ACL writer or Docker socket is
166
- given to the App. Grant directly to the package account; supplementary DSM groups
167
- are not added to the container. See the SDK Shared Folder permission guide.
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`, use matching `install.sharedFolder: {mode: select, key: media, permission: ro}` and pass `--param media=Media`. Upgrade keeps the selection unless `--param CHANGE_media=true --param media=OtherMedia` is provided. An administrator grants the fixed package account access through DSM; selection is not a grant. 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.
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` 使用套件帳號:select 模式安裝後可能須管理員手動授予權限,create 由 DSM data-share 建立;都不是逐使用者 session access。移除不刪共用資料夾。Delete App Data 不是完整清理證明,須讀回殘留。
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.18.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).