@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,3 +1,4 @@
|
|
|
1
|
+
import { portDefaults, portInstallSteps } from "./install-ports.js";
|
|
1
2
|
import { inspectPackageDependencies, dependencyInfo } from "./package-dependencies.js";
|
|
2
3
|
import { readInstallSettings, settingsInstallSteps, settingsConfig, renderSettingsHook } from './install-settings.js';
|
|
3
4
|
import { MODULE_UI_ENTRY, uiTreeDigest } from "./ui-assets.js";
|
|
@@ -217,7 +218,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
217
218
|
!isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
|
|
218
219
|
findings.push(`INFO os_min_ver "${info.os_min_ver}" is below the DSM ${PORTAL_MIN_DSM} a portal needs; ${PORTAL_MIN_DSM_REASON}`);
|
|
219
220
|
}
|
|
220
|
-
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver, daemonIngress);
|
|
221
|
+
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver, daemonIngress, Boolean(metadata.installPorts));
|
|
221
222
|
checkScripts(dir, findings);
|
|
222
223
|
checkUninstall(dir, findings);
|
|
223
224
|
if (kind === "container")
|
|
@@ -318,7 +319,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
318
319
|
? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
|
|
319
320
|
: undefined;
|
|
320
321
|
if (standalone)
|
|
321
|
-
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor, nativeGateway, info.os_min_ver, installSettings, integrations);
|
|
322
|
+
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor, nativeGateway, info.os_min_ver, installSettings, integrations, metadata.installPorts);
|
|
322
323
|
const icon = inspectPackageIcons(dir, findings);
|
|
323
324
|
const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
|
|
324
325
|
if (standalone && !cgiEntry) {
|
|
@@ -744,6 +745,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
744
745
|
composeMounts: metadata.capabilities?.includes("composeMounts"),
|
|
745
746
|
sharedFolder: recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))),
|
|
746
747
|
installSettings: metadata.installSettings,
|
|
748
|
+
installPorts: metadata.installPorts,
|
|
747
749
|
architecture: arch,
|
|
748
750
|
imageProofs: JSON.parse(readFileSync(join(root, "compose/images.json"), "utf8")),
|
|
749
751
|
});
|
|
@@ -792,6 +794,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
792
794
|
},
|
|
793
795
|
compose: c,
|
|
794
796
|
sharedFolder: recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))),
|
|
797
|
+
...(metadata.installPorts ? { installPorts: true } : {}),
|
|
795
798
|
...(g ? { identityGateway: g } : {}),
|
|
796
799
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
797
800
|
};
|
|
@@ -801,8 +804,8 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
801
804
|
throw new Error("gateway configuration differs from the declared package contract");
|
|
802
805
|
if (readOptional(join(dir, "conf/privilege")) !== renderNativePrivilege())
|
|
803
806
|
throw new Error("Compose hooks must run as the package account");
|
|
804
|
-
if (manifest.sharedFolder || metadata.installSettings) {
|
|
805
|
-
for (const [name, text] of Object.entries(expectedWizardFiles(info.package, manifest.sharedFolder, false, undefined, metadata.installSettings)))
|
|
807
|
+
if (manifest.sharedFolder || metadata.installSettings || metadata.installPorts) {
|
|
808
|
+
for (const [name, text] of Object.entries(expectedWizardFiles(info.package, manifest.sharedFolder, false, undefined, metadata.installSettings, [], manifest.identityGateway?.listen.https, metadata.installPorts)))
|
|
806
809
|
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
807
810
|
throw new Error(`unexpected WIZARD_UIFILES/${name}; native share selection must match the declaration`);
|
|
808
811
|
}
|
|
@@ -837,7 +840,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
837
840
|
"postupgrade",
|
|
838
841
|
"start-stop-status",
|
|
839
842
|
])
|
|
840
|
-
if (!matchesStandaloneScript(hook, manifest, readOptional(join(dir, "scripts", hook))))
|
|
843
|
+
if (!matchesStandaloneScript(hook, manifest, readOptional(join(dir, "scripts", hook)), Boolean(metadata.installPorts)))
|
|
841
844
|
throw new Error(`unexpected scripts/${hook}`);
|
|
842
845
|
if (appScripts.length)
|
|
843
846
|
base.appScripts = appScripts;
|
|
@@ -1310,12 +1313,13 @@ async function recoverDaemonIngress(dir, value, info, paths, findings) {
|
|
|
1310
1313
|
return undefined;
|
|
1311
1314
|
}
|
|
1312
1315
|
}
|
|
1313
|
-
function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose, nativeGateway, osMinVer, daemonIngress) {
|
|
1316
|
+
function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose, nativeGateway, osMinVer, daemonIngress, installPorts = false) {
|
|
1314
1317
|
const confDir = join(dir, "conf");
|
|
1315
1318
|
const entries = existsSync(confDir) ? readdirSync(confDir) : [];
|
|
1316
1319
|
const nativeResource = kind !== "container"
|
|
1317
1320
|
? renderStandaloneResource({
|
|
1318
1321
|
package: pkg ?? "",
|
|
1322
|
+
...(installPorts ? { installPorts: true } : {}),
|
|
1319
1323
|
os_min_ver: osMinVer,
|
|
1320
1324
|
...(nativeGateway ? { identityGateway: nativeGateway } : {}),
|
|
1321
1325
|
...(compose ? { compose } : {}),
|
|
@@ -1415,7 +1419,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
|
|
|
1415
1419
|
* declaration (without one, any present file must be the default native
|
|
1416
1420
|
* install wizard), and no other install/upgrade wizard file may exist.
|
|
1417
1421
|
*/
|
|
1418
|
-
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false, nativeGateway, osMinVer, installSettings, integrations = []) {
|
|
1422
|
+
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false, nativeGateway, osMinVer, installSettings, integrations = [], installPortDefinitions = {}) {
|
|
1419
1423
|
const wizardDir = join(dir, "WIZARD_UIFILES");
|
|
1420
1424
|
const present = (existsSync(wizardDir) ? readdirSync(wizardDir) : [])
|
|
1421
1425
|
.filter((name) => /^(install|upgrade)_uifile(_|\.|$)/.test(name))
|
|
@@ -1431,6 +1435,7 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1431
1435
|
package: pkg,
|
|
1432
1436
|
os_min_ver: osMinVer,
|
|
1433
1437
|
sharedFolder: folder,
|
|
1438
|
+
...(Object.keys(installPortDefinitions).length ? { installPorts: true } : {}),
|
|
1434
1439
|
capabilities: selected
|
|
1435
1440
|
? { delegate: { scope: "install-selected" } }
|
|
1436
1441
|
: executor
|
|
@@ -1441,7 +1446,7 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1441
1446
|
: {}),
|
|
1442
1447
|
...portalManifest(portal),
|
|
1443
1448
|
};
|
|
1444
|
-
const wizards = expectedWizardFiles(pkg, folder, selected, portal, installSettings, integrations);
|
|
1449
|
+
const wizards = expectedWizardFiles(pkg, folder, selected, portal, installSettings, integrations, nativeGateway?.listen.https, installPortDefinitions);
|
|
1445
1450
|
const source = selected
|
|
1446
1451
|
? "the install-selected policy"
|
|
1447
1452
|
: folder
|
|
@@ -1452,7 +1457,7 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1452
1457
|
? "the integrations declaration"
|
|
1453
1458
|
: "the native-go default install wizard";
|
|
1454
1459
|
// The default native wizard is optional; a declared shared folder or portal needs all of its files.
|
|
1455
|
-
for (const name of folder || selected || portal || installSettings || integrations.length
|
|
1460
|
+
for (const name of folder || selected || portal || installSettings || Object.keys(installPortDefinitions).length || integrations.length
|
|
1456
1461
|
? Object.keys(wizards)
|
|
1457
1462
|
: present.filter((n) => n in wizards)) {
|
|
1458
1463
|
if (readOptional(join(wizardDir, name)) !== wizards[name])
|
|
@@ -1462,14 +1467,14 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1462
1467
|
findings.push(`WIZARD_UIFILES/${name} is not a wizard file produced by ${source}`);
|
|
1463
1468
|
if (selected) {
|
|
1464
1469
|
for (const hook of ["preinst", "postinst"]) {
|
|
1465
|
-
if (!matchesStandaloneScript(hook, lifecycleManifest, readOptional(join(dir, "scripts", hook))))
|
|
1470
|
+
if (!matchesStandaloneScript(hook, lifecycleManifest, readOptional(join(dir, "scripts", hook)), Boolean(Object.keys(installPortDefinitions).length)))
|
|
1466
1471
|
findings.push(`scripts/${hook} does not match the fixed install-selected draft handling`);
|
|
1467
1472
|
}
|
|
1468
1473
|
}
|
|
1469
1474
|
if (!folder && !portal)
|
|
1470
1475
|
return;
|
|
1471
1476
|
for (const hook of ["postinst", "postupgrade"]) {
|
|
1472
|
-
if (!matchesStandaloneScript(hook, lifecycleManifest, readOptional(join(dir, "scripts", hook)))) {
|
|
1477
|
+
if (!matchesStandaloneScript(hook, lifecycleManifest, readOptional(join(dir, "scripts", hook)), Boolean(Object.keys(installPortDefinitions).length))) {
|
|
1473
1478
|
findings.push(`scripts/${hook} does not match the fixed script declared by ${folder ? "install.sharedFolder" : "portal"}`);
|
|
1474
1479
|
}
|
|
1475
1480
|
}
|
|
@@ -1479,7 +1484,8 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1479
1484
|
async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
1480
1485
|
const settings = readInstallSettings(metadata.installSettings, e => findings.push(e.message));
|
|
1481
1486
|
const present = payload.filter(p => p.path.startsWith('install/'));
|
|
1482
|
-
|
|
1487
|
+
const ports = metadata.installPorts;
|
|
1488
|
+
if (!settings && !ports) {
|
|
1483
1489
|
if (present.length)
|
|
1484
1490
|
findings.push('INSTALL_SETTINGS_INVALID unexpected installation settings payload');
|
|
1485
1491
|
return;
|
|
@@ -1489,12 +1495,30 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1489
1495
|
await tar.x({ file: join(dir, 'package.tgz'), cwd: root });
|
|
1490
1496
|
const info = readInfo(join(dir, 'INFO'), findings, 'binary', Boolean(metadata.compose), undefined, inspectPackageDependencies(metadata.dependencies, findings));
|
|
1491
1497
|
const gateway = metadata.compose?.gateway ?? metadata.native_gateway;
|
|
1492
|
-
const m = { package: info.package, os_min_ver: info.os_min_ver, capabilities: {}, installSettings: settings, identityGateway: gateway };
|
|
1498
|
+
const m = { package: info.package, os_min_ver: info.os_min_ver, capabilities: {}, installSettings: settings, identityGateway: gateway, ...(ports ? { installPorts: true } : {}) };
|
|
1493
1499
|
try {
|
|
1494
|
-
if (readOptional(join(root, 'install/settings.json')) !== JSON.stringify(settingsConfig(m), null, 2) + '\n')
|
|
1500
|
+
if (settings && readOptional(join(root, 'install/settings.json')) !== JSON.stringify(settingsConfig(m), null, 2) + '\n')
|
|
1495
1501
|
throw new Error('settings inventory differs from its declaration or gateway port');
|
|
1496
|
-
|
|
1502
|
+
const allowed = ['install/control', ...(settings ? ['install/settings.json'] : []), ...(ports ? ['install/ports.json', `install/${info.package}.sc`] : [])];
|
|
1503
|
+
if (present.length !== allowed.length || !present.some(p => p.path === 'install/control' && p.mode === '0755') || present.some(p => !allowed.includes(p.path)))
|
|
1497
1504
|
throw new Error('only SDK control and declarations may be packaged; values must never ship');
|
|
1505
|
+
if (ports) {
|
|
1506
|
+
const config = JSON.parse(readFileSync(join(root, 'install/ports.json'), 'utf8'));
|
|
1507
|
+
const actualGateway = JSON.parse(readFileSync(join(root, 'sso/gateway.json'), 'utf8'));
|
|
1508
|
+
const expected = portDefaults({ ...m, identityGateway: { ...actualGateway, sdk: gateway.sdk } });
|
|
1509
|
+
if (config.schema !== 1 || config.package !== info.package || !isDeepStrictEqual(config.ports, ports) || !isDeepStrictEqual(ports, expected))
|
|
1510
|
+
throw new Error('port defaults differ from gateway listeners');
|
|
1511
|
+
const required = ['sso/gateway.json', 'daemon/config.json', ...(actualGateway.native_ingress ? ['sso/native-nginx.conf'] : []), ...(settings ? ['install/settings.json'] : []), ...payload.filter(p => /^ui\/window-[0-9]+\.js$/.test(p.path)).map(p => p.path)].sort();
|
|
1512
|
+
if (!isDeepStrictEqual(Object.keys(config.files).sort(), required))
|
|
1513
|
+
throw new Error('port templates must cover every generated port consumer');
|
|
1514
|
+
for (const [path, source] of Object.entries(config.files)) {
|
|
1515
|
+
let text = String(source);
|
|
1516
|
+
for (const [key, p] of Object.entries(ports))
|
|
1517
|
+
text = text.replaceAll('@@PORT_' + key + '@@', String(p.default));
|
|
1518
|
+
if (text.includes('@@PORT_') || readOptional(join(root, path)) !== text)
|
|
1519
|
+
throw new Error('port template differs from generated default artifact');
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1498
1522
|
const arch = info.arch.split(/\s+/).map(familyOfArchToken).find(Boolean) ?? 'x86_64';
|
|
1499
1523
|
const error = checkCgiBinary(root, join(root, 'install/control'), arch);
|
|
1500
1524
|
if (error)
|
|
@@ -1513,7 +1537,7 @@ async function inspectInstallSettings(dir, payload, metadata, findings) {
|
|
|
1513
1537
|
}
|
|
1514
1538
|
return settings;
|
|
1515
1539
|
}
|
|
1516
|
-
function expectedWizardFiles(pkg, folder, selected = false, portal, installSettings, integrations = []) {
|
|
1540
|
+
function expectedWizardFiles(pkg, folder, selected = false, portal, installSettings, integrations = [], settingsPort = 0, installPortDefinitions = {}) {
|
|
1517
1541
|
const steps = selected
|
|
1518
1542
|
? (t) => [
|
|
1519
1543
|
...renderDelegateScopeSteps(t),
|
|
@@ -1522,7 +1546,7 @@ function expectedWizardFiles(pkg, folder, selected = false, portal, installSetti
|
|
|
1522
1546
|
: folder
|
|
1523
1547
|
? sharedFolderInstallSteps(pkg, folder)
|
|
1524
1548
|
: renderNativeInstallWizard;
|
|
1525
|
-
const install = renderInstallWizardFiles(pkg, t => [...steps(t), ...settingsInstallSteps(installSettings)(t), ...(portal ? [renderPortalWizardStep(t, portal.alias)] : []), ...integrationWizardSteps(t, integrations)]);
|
|
1549
|
+
const install = renderInstallWizardFiles(pkg, t => [...steps(t), ...portInstallSteps({ package: pkg, ...(Object.keys(installPortDefinitions).length ? { installPorts: true } : {}) }, installPortDefinitions)(t), ...settingsInstallSteps(installSettings, settingsPort, Object.keys(installPortDefinitions).length ? pkg : undefined)(t), ...(portal ? [renderPortalWizardStep(t, portal.alias)] : []), ...integrationWizardSteps(t, integrations)]);
|
|
1526
1550
|
return {
|
|
1527
1551
|
...install.static,
|
|
1528
1552
|
...install.dynamic,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { StandaloneManifest } from './manifest.js';
|
|
2
|
+
import type { Translate } from '../build-spk/wizard-i18n.js';
|
|
3
|
+
export declare function portDefaults(m: Pick<StandaloneManifest, 'identityGateway' | 'installPorts'>): Record<string, {
|
|
4
|
+
default: number;
|
|
5
|
+
protocol: 'tcp';
|
|
6
|
+
}>;
|
|
7
|
+
export declare function withPort(m: StandaloneManifest, key: string, value: number): StandaloneManifest;
|
|
8
|
+
export declare function portFieldValidator(pkg: string, key: string, reason: string): string;
|
|
9
|
+
export declare function portInstallSteps(m: Pick<StandaloneManifest, 'package' | 'installPorts' | 'identityGateway'>, definitions?: Record<string, {
|
|
10
|
+
default: number;
|
|
11
|
+
protocol: "tcp";
|
|
12
|
+
}>): (t: Translate) => {
|
|
13
|
+
step_title: string;
|
|
14
|
+
items: {
|
|
15
|
+
type: string;
|
|
16
|
+
desc: string;
|
|
17
|
+
subitems: {
|
|
18
|
+
key: string;
|
|
19
|
+
desc: string;
|
|
20
|
+
defaultValue: string;
|
|
21
|
+
validator: {
|
|
22
|
+
allowBlank: boolean;
|
|
23
|
+
fn: string;
|
|
24
|
+
};
|
|
25
|
+
}[];
|
|
26
|
+
}[];
|
|
27
|
+
}[];
|
|
28
|
+
/** Render each artifact with out-of-range marker ports. Only values derived
|
|
29
|
+
* from the selected listener change; an unrelated timer or numeric setting
|
|
30
|
+
* equal to a default port is preserved. Originals remain inspectable in SPK. */
|
|
31
|
+
export declare function writePortPayload(m: StandaloneManifest, root: string, render: (variant: StandaloneManifest) => Record<string, string>): void;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export function portDefaults(m) {
|
|
4
|
+
if (!m.installPorts || !m.identityGateway)
|
|
5
|
+
return {};
|
|
6
|
+
const g = m.identityGateway;
|
|
7
|
+
return Object.fromEntries([
|
|
8
|
+
...Object.entries(g.listen ?? {}).map(([key, value]) => ['gateway_' + key, { default: value, protocol: 'tcp' }]),
|
|
9
|
+
...(g.oidc ? [['oidc_issuer', { default: g.oidc.issuer_port, protocol: 'tcp' }]] : []),
|
|
10
|
+
]);
|
|
11
|
+
}
|
|
12
|
+
export function withPort(m, key, value) {
|
|
13
|
+
const g = m.identityGateway;
|
|
14
|
+
return { ...m, identityGateway: { ...g, listen: { ...g.listen, ...(key.startsWith('gateway_') ? { [key.slice(8)]: value } : {}) }, ...(g.oidc ? { oidc: { ...g.oidc, ...(key === 'oidc_issuer' ? { issuer_port: value } : {}) } } : {}) } };
|
|
15
|
+
}
|
|
16
|
+
function validatePortField(value, pkg, key, reason) {
|
|
17
|
+
const raw = String(value), n = Number(raw);
|
|
18
|
+
if (!/^[1-9][0-9]{3,4}$/.test(raw) || n < 1024 || n > 65535 || [1900, 5000, 5001, 5005, 5006, 5353, 6690].includes(n))
|
|
19
|
+
return reason;
|
|
20
|
+
try {
|
|
21
|
+
let request = new XMLHttpRequest();
|
|
22
|
+
request.open('GET', '/webman/login.cgi?enable_syno_token=yes', false);
|
|
23
|
+
request.send();
|
|
24
|
+
const token = JSON.parse(request.responseText).SynoToken;
|
|
25
|
+
if (!token)
|
|
26
|
+
throw new Error();
|
|
27
|
+
request = new XMLHttpRequest();
|
|
28
|
+
request.open('POST', '/webapi/entry.cgi', false);
|
|
29
|
+
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
30
|
+
request.setRequestHeader('X-SYNO-TOKEN', token);
|
|
31
|
+
request.send('api=SYNO.Core.Service.PortInfo&version=1&method=load');
|
|
32
|
+
const result = JSON.parse(request.responseText), rows = result.data?.port_info;
|
|
33
|
+
if (request.status !== 200 || result.success !== true || !Array.isArray(rows) || !rows.length)
|
|
34
|
+
throw new Error();
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
if (typeof row.protocol !== 'string' || typeof row.port_id !== 'string' || !Array.isArray(row.dst_port) || !row.dst_port.length)
|
|
37
|
+
throw new Error();
|
|
38
|
+
const protocol = row.protocol.toLowerCase().replace(/\s/g, '');
|
|
39
|
+
if (!/^(?:tcp|udp)(?:,(?:tcp|udp))*$/.test(protocol))
|
|
40
|
+
throw new Error();
|
|
41
|
+
if (!protocol.split(',').includes('tcp'))
|
|
42
|
+
continue;
|
|
43
|
+
for (const item of row.dst_port) {
|
|
44
|
+
if (typeof item !== 'string')
|
|
45
|
+
throw new Error();
|
|
46
|
+
const range = /^([0-9]+)(?:[-:]([0-9]+))?$/.exec(item);
|
|
47
|
+
if (!range || Number(range[1]) < 1 || Number(range[2] || range[1]) > 65535 || Number(range[1]) > Number(range[2] || range[1]))
|
|
48
|
+
throw new Error();
|
|
49
|
+
if (n >= Number(range[1]) && n <= Number(range[2] || range[1]) && row.port_id !== 'synopkgland.' + pkg.replaceAll('_', '-') + '.' + key.replaceAll('_', '-'))
|
|
50
|
+
return reason;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const globals = globalThis;
|
|
54
|
+
const state = globals['synopkglandPorts_' + pkg] || (globals['synopkglandPorts_' + pkg] = {});
|
|
55
|
+
for (const other in state)
|
|
56
|
+
if (other !== key && state[other] === n)
|
|
57
|
+
return reason;
|
|
58
|
+
state[key] = n;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return reason;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function portFieldValidator(pkg, key, reason) {
|
|
66
|
+
return `{return (${validatePortField.toString()})(arguments[0],${JSON.stringify(pkg)},${JSON.stringify(key)},${JSON.stringify(reason)})}`;
|
|
67
|
+
}
|
|
68
|
+
export function portInstallSteps(m, definitions = portDefaults(m)) {
|
|
69
|
+
return (t) => m.installPorts ? [{ step_title: t('ports.title'), items: Object.entries(definitions).map(([key, p]) => ({ type: 'textfield', desc: t(key === 'gateway_http' ? 'ports.http' : key === 'gateway_https' ? 'ports.https' : 'ports.signin'), subitems: [{ key: 'pkgwizard_port_' + key, desc: t(key === 'gateway_http' ? 'ports.http' : key === 'gateway_https' ? 'ports.https' : 'ports.signin'), defaultValue: String(p.default), validator: { allowBlank: false, fn: portFieldValidator(m.package, key, t('ports.invalid')) } }] })) }] : [];
|
|
70
|
+
}
|
|
71
|
+
/** Render each artifact with out-of-range marker ports. Only values derived
|
|
72
|
+
* from the selected listener change; an unrelated timer or numeric setting
|
|
73
|
+
* equal to a default port is preserved. Originals remain inspectable in SPK. */
|
|
74
|
+
export function writePortPayload(m, root, render) {
|
|
75
|
+
if (!m.installPorts)
|
|
76
|
+
return;
|
|
77
|
+
const ports = portDefaults(m), base = render(m);
|
|
78
|
+
let marker = 900000000;
|
|
79
|
+
if (!Object.keys(ports).length)
|
|
80
|
+
throw new Error('INSTALL_PORT_INVALID: install.ports requires an identity gateway');
|
|
81
|
+
const marked = Object.entries(ports).reduce((variant, [key]) => {
|
|
82
|
+
while (Object.values(base).some(s => s.includes(String(marker))))
|
|
83
|
+
marker++;
|
|
84
|
+
const value = marker++;
|
|
85
|
+
return withPort(variant, key, value);
|
|
86
|
+
}, m);
|
|
87
|
+
const files = render(marked);
|
|
88
|
+
let index = 0;
|
|
89
|
+
for (const [key] of Object.entries(ports)) {
|
|
90
|
+
const g = marked.identityGateway;
|
|
91
|
+
const value = key === 'oidc_issuer' ? g.oidc.issuer_port : g.listen[key.slice(8)];
|
|
92
|
+
for (const path of Object.keys(files))
|
|
93
|
+
files[path] = files[path].replaceAll(String(value), '@@PORT_' + key + '@@');
|
|
94
|
+
index++;
|
|
95
|
+
}
|
|
96
|
+
for (const [path, content] of Object.entries(base)) {
|
|
97
|
+
if (!existsSync(join(root, path)) || readFileSync(join(root, path), 'utf8') !== content)
|
|
98
|
+
throw new Error('INSTALL_PORT_INVALID: port template must match the generated artifact ' + path);
|
|
99
|
+
}
|
|
100
|
+
mkdirSync(join(root, 'install'), { recursive: true });
|
|
101
|
+
writeFileSync(join(root, 'install/ports.json'), JSON.stringify({ schema: 1, package: m.package, ports, files }, null, 2) + '\n', { mode: 0o600 });
|
|
102
|
+
writeFileSync(join(root, `install/${m.package}.sc`), Object.entries(ports).map(([key, p]) => `[synopkgland.${m.package.replaceAll('_', '-')}.${key.replaceAll('_', '-')}]\ntitle="${m.package}"\ndesc="${m.package}"\nport_forward="no"\ndst.ports="${p.default}/${p.protocol}"\n`).join('\n'), { mode: 0o644 });
|
|
103
|
+
}
|
|
@@ -9,7 +9,8 @@ export interface InstallSetting {
|
|
|
9
9
|
}
|
|
10
10
|
export type InstallSettings = Record<string, InstallSetting>;
|
|
11
11
|
export declare function readInstallSettings(value: unknown, fail: (e: StandaloneError) => void): InstallSettings | undefined;
|
|
12
|
-
export declare function
|
|
12
|
+
export declare function originFieldValidator(required: boolean, port: number, reason: string, portPackage?: string): string;
|
|
13
|
+
export declare function settingsInstallSteps(settings: InstallSettings | undefined, port?: number, portPackage?: string): (t: Translate) => {
|
|
13
14
|
step_title: string;
|
|
14
15
|
items: {
|
|
15
16
|
type: string;
|
|
@@ -20,6 +21,7 @@ export declare function settingsInstallSteps(settings: InstallSettings | undefin
|
|
|
20
21
|
defaultValue: string;
|
|
21
22
|
validator: {
|
|
22
23
|
allowBlank: boolean;
|
|
24
|
+
fn: string;
|
|
23
25
|
};
|
|
24
26
|
}[];
|
|
25
27
|
}[];
|
|
@@ -23,16 +23,22 @@ export function readInstallSettings(value, fail) {
|
|
|
23
23
|
}
|
|
24
24
|
return Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)));
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
// DSM invokes validator.fn with the field value as arguments[0]. Keep this
|
|
27
|
+
// independent of App JavaScript: the package has not been installed yet.
|
|
28
|
+
export function originFieldValidator(required, port, reason, portPackage) {
|
|
29
|
+
const expected = portPackage ? `(globalThis[${JSON.stringify('synopkglandPorts_' + portPackage)}]?.gateway_https??${port})` : String(port);
|
|
30
|
+
return `{var expected=${expected};var value=arguments[0];if(!value)return ${required ? JSON.stringify(reason) : 'true'};try{if(typeof value!=='string'||value.length>2048||/[\\x00-\\x20\\x7f]/.test(value)||!/^https:\\/\\//.test(value))return ${JSON.stringify(reason)};var u=new URL(value);var host=u.hostname;if(!host||host.length>253||host.indexOf(':')>=0||!host.split('.').every(function(label){return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(label)}))return ${JSON.stringify(reason)};var authority=value.slice(8);if(/[\\/\\?#@\\\\]/.test(authority))return ${JSON.stringify(reason)};var parts=authority.split(':');if(!parts[0].split('.').every(function(label){return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(label)}))return ${JSON.stringify(reason)};if(parts.length>2||(parts.length===2&&(!/^[1-9][0-9]{0,4}$/.test(parts[1])||Number(parts[1])>65535)))return ${JSON.stringify(reason)};if(expected&&parts[1]!==String(expected))return ${JSON.stringify(reason)};return true}catch(e){return ${JSON.stringify(reason)}}}`;
|
|
31
|
+
}
|
|
32
|
+
export function settingsInstallSteps(settings, port = 0, portPackage) {
|
|
27
33
|
return (t) => settings ? [{ step_title: t('install.step'), items: Object.entries(settings).map(([key, s]) => ({
|
|
28
|
-
type: 'textfield', desc:
|
|
34
|
+
type: 'textfield', desc: s.label, subitems: [{ key: `pkgwizard_setting_${key}`, desc: s.label, defaultValue: '', validator: { allowBlank: !s.required, fn: originFieldValidator(s.required, port, portPackage ? t('ports.originInvalid') : t('settings.originInvalid', { port: port ? `:${port}` : '' }), portPackage) } }],
|
|
29
35
|
})) }] : [];
|
|
30
36
|
}
|
|
31
37
|
export function settingsConfig(m) {
|
|
32
38
|
return { schema: 1, package: m.package, ...(m.identityGateway?.listen.https ? { https_port: m.identityGateway.listen.https } : {}), settings: m.installSettings };
|
|
33
39
|
}
|
|
34
40
|
export function renderSettingsHook(name, m) {
|
|
35
|
-
if (!m.installSettings)
|
|
41
|
+
if (!m.installSettings && !m.installPorts)
|
|
36
42
|
return '';
|
|
37
43
|
const action = name === 'preinst' ? 'prepare' : ['postinst', 'postupgrade'].includes(name) ? 'remember' : name === 'preuninst' ? 'purge' : undefined;
|
|
38
44
|
if (!action)
|
|
@@ -41,11 +47,13 @@ export function renderSettingsHook(name, m) {
|
|
|
41
47
|
return `"${root}/install/control" ${action} --root "${root}" --package '${m.package}' || exit $?\n`;
|
|
42
48
|
}
|
|
43
49
|
export async function buildInstallSettings(m, arch, work, run) {
|
|
44
|
-
if (!m.installSettings)
|
|
50
|
+
if (!m.installSettings && !m.installPorts)
|
|
45
51
|
return;
|
|
46
52
|
const sdk = m.compose?.sdk ?? m.identityGateway?.sdk ?? templateSdkVersion(), source = sdkSource(sdk);
|
|
47
53
|
if (!existsSync(join(source.goDir, 'installsettings/cmd/control/main.go')))
|
|
48
54
|
throw new StandaloneBuildError('SDK_INSTALL_SETTINGS_UNSUPPORTED', 'Use an SDK release with native install.settings support.');
|
|
55
|
+
if (m.installPorts && !existsSync(join(source.goDir, 'installsettings/ports.go')))
|
|
56
|
+
throw new StandaloneBuildError('SDK_PORT_SETTINGS_UNSUPPORTED', 'Use an SDK release with install.ports support');
|
|
49
57
|
const out = join(work, `install-settings-${arch}`);
|
|
50
58
|
const result = await run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', out, './installsettings/cmd/control'], { cwd: source.goDir, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }) });
|
|
51
59
|
if (result.status !== 0)
|
|
@@ -5,4 +5,4 @@ export declare function renderStandaloneScript(name: string, manifest: Standalon
|
|
|
5
5
|
* The backend inspector then replays the entire remaining lifecycle script. */
|
|
6
6
|
export declare function matchesStandaloneScriptPrefix(name: string, manifest: StandaloneManifest, actual: string): boolean;
|
|
7
7
|
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
8
|
-
export declare function matchesStandaloneScript(name: string, manifest: StandaloneManifest, actual: string | undefined): boolean;
|
|
8
|
+
export declare function matchesStandaloneScript(name: string, manifest: StandaloneManifest, actual: string | undefined, settingsPeeled?: boolean): boolean;
|
|
@@ -35,8 +35,9 @@ export function matchesStandaloneScriptPrefix(name, manifest, actual) {
|
|
|
35
35
|
blocks.every(block => prefix.includes(block) || !actual.includes(block)));
|
|
36
36
|
}
|
|
37
37
|
/** Accept the exact current, diagnostic-only, and released lifecycle variants. */
|
|
38
|
-
export function matchesStandaloneScript(name, manifest, actual) {
|
|
39
|
-
|
|
38
|
+
export function matchesStandaloneScript(name, manifest, actual, settingsPeeled = false) {
|
|
39
|
+
const expected = renderStandaloneScript(name, manifest);
|
|
40
|
+
return actual !== undefined && standaloneScriptVariants(name, manifest, settingsPeeled ? expected.replace(renderSettingsHook(name, manifest), '') : expected).includes(actual);
|
|
40
41
|
}
|
|
41
42
|
function standaloneScriptVariants(name, manifest, expected) {
|
|
42
43
|
const diagnostic = renderNativeGatewayDiagnostic(manifest, name);
|
|
@@ -102,6 +102,7 @@ export interface StandaloneManifest {
|
|
|
102
102
|
/** `install.sharedFolder`: the Shared Folder the install wizard selects or `data-share` creates. */
|
|
103
103
|
sharedFolder?: SharedFolder;
|
|
104
104
|
installSettings?: InstallSettings;
|
|
105
|
+
installPorts?: true;
|
|
105
106
|
/** `backup`: Hyper Backup application backup; on by default for native Apps, absent when opted out. */
|
|
106
107
|
hyperBackup?: HyperBackup;
|
|
107
108
|
/** Who may use the app at all; absent keeps the route policy alone. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { portDefaults } from "./install-ports.js";
|
|
1
2
|
import { readPackageDependencies, packageDependencies, dependencyError } from "./package-dependencies.js";
|
|
2
3
|
import { readInstallSettings } from './install-settings.js';
|
|
3
4
|
import { auditGoRoutes } from './route-audit.js';
|
|
@@ -271,6 +272,9 @@ function readManifest(raw, c) {
|
|
|
271
272
|
const sharedFolder = raw.install === undefined
|
|
272
273
|
? undefined
|
|
273
274
|
: readSharedFolder(raw.install, (e) => c.fail(e.code, e.message));
|
|
275
|
+
const installPorts = isRecord(raw.install) && raw.install.ports === true ? true : undefined;
|
|
276
|
+
if (isRecord(raw.install) && raw.install.ports !== undefined && raw.install.ports !== true)
|
|
277
|
+
c.fail("INSTALL_PORT_INVALID", "install.ports must be true when present");
|
|
274
278
|
const installSettings = readInstallSettings(isRecord(raw.install) ? raw.install.settings : undefined, e => c.fail(e.code, e.message));
|
|
275
279
|
const hyperBackup = readHyperBackup(raw.backup, { compose: raw.compose !== undefined }, e => c.fail(e.code, e.message));
|
|
276
280
|
const compose = arches &&
|
|
@@ -281,6 +285,7 @@ function readManifest(raw, c) {
|
|
|
281
285
|
composeMounts: capabilities.composeMounts,
|
|
282
286
|
sharedFolder,
|
|
283
287
|
installSettings,
|
|
288
|
+
installPorts: installPorts ? portDefaults({ installPorts, identityGateway: gatewayRaw }) : undefined,
|
|
284
289
|
});
|
|
285
290
|
if (raw.compose !== undefined &&
|
|
286
291
|
((raw.daemon !== undefined && !timeoutOnly) ||
|
|
@@ -303,6 +308,16 @@ function readManifest(raw, c) {
|
|
|
303
308
|
c.fail("COMPOSE_WEB_INVALID", "Compose upstream is derived from the SDK Compose ingress endpoint; remove identity_gateway.upstream.");
|
|
304
309
|
// A failed Compose plan already explains the invalid upstream declaration.
|
|
305
310
|
const identityGateway = raw.compose !== undefined && !compose ? undefined : readIdentityGateway(gatewayValue, daemon || (compose ? { targets: compose.targets } : undefined), (e) => c.fail(e.code, e.message), false, Boolean(compose?.plan.ingress));
|
|
311
|
+
if (installPorts && !identityGateway)
|
|
312
|
+
c.fail('INSTALL_PORT_INVALID', 'install.ports requires an identity gateway');
|
|
313
|
+
if (installPorts && identityGateway) {
|
|
314
|
+
const [major, minor, patch] = identityGateway.sdk.split('.').map(Number);
|
|
315
|
+
if (major === 0 && (minor < 38 || minor === 38 && patch < 2))
|
|
316
|
+
c.fail('SDK_PORT_SETTINGS_UNSUPPORTED', 'install.ports requires SDK 0.38.2 or newer');
|
|
317
|
+
for (const p of Object.values(portDefaults({ identityGateway, installPorts })))
|
|
318
|
+
if (!Number.isInteger(p.default) || p.default < 1024 || p.default > 65535 || [1900, 5000, 5001, 5005, 5006, 5353, 6690].includes(p.default))
|
|
319
|
+
c.fail('INSTALL_PORT_INVALID', 'Choose non-reserved manifest port defaults from 1024 to 65535');
|
|
320
|
+
}
|
|
306
321
|
if (daemon?.ingress) {
|
|
307
322
|
const problem = daemonIngressMinDsmProblem(osMinVer);
|
|
308
323
|
if (problem)
|
|
@@ -550,6 +565,7 @@ function readManifest(raw, c) {
|
|
|
550
565
|
...(uninstall ? { uninstall } : {}),
|
|
551
566
|
...(sharedFolder ? { sharedFolder } : {}),
|
|
552
567
|
...(installSettings ? { installSettings } : {}),
|
|
568
|
+
...(installPorts ? { installPorts } : {}),
|
|
553
569
|
...(hyperBackup ? { hyperBackup } : {}),
|
|
554
570
|
...(entry ? { entry } : {}),
|
|
555
571
|
...(integrations?.length ? { integrations } : {}),
|
|
@@ -14,6 +14,8 @@ export function nativeGatewaySupported(m) {
|
|
|
14
14
|
export function renderNativeGatewayDiagnostic(m, hook) {
|
|
15
15
|
if (!nativeGatewaySupported(m) || !['start-stop-status', 'postinst', 'postupgrade', 'preupgrade', 'preuninst'].includes(hook))
|
|
16
16
|
return '';
|
|
17
|
+
if (m.installPorts)
|
|
18
|
+
return managedPortHook(m, hook, false);
|
|
17
19
|
const ports = [...new Set([m.identityGateway.listen.https, m.identityGateway.listen.http].filter((port) => port !== undefined))];
|
|
18
20
|
const action = hook === 'start-stop-status'
|
|
19
21
|
? ` case "\${1:-}" in prestart|prestop|start|stop|restart) action="$1" ;; *) exit 0 ;; esac\n`
|
|
@@ -43,6 +45,8 @@ synopkgland_native_diagnostic "$@" >/dev/null 2>&1 || :
|
|
|
43
45
|
export function renderNativeGatewayPortWait(m, hook) {
|
|
44
46
|
if (hook !== 'start-stop-status' || !nativeGatewaySupported(m))
|
|
45
47
|
return '';
|
|
48
|
+
if (m.installPorts)
|
|
49
|
+
return managedPortHook(m, hook, true);
|
|
46
50
|
const ports = [...new Set([m.identityGateway.listen.https, m.identityGateway.listen.http].filter((port) => port !== undefined))];
|
|
47
51
|
return `synopkgland_native_port_wait() (
|
|
48
52
|
[ "\${1:-}" = prestart ] || exit 0
|
|
@@ -108,7 +112,7 @@ export function nativeGatewayResource(m) {
|
|
|
108
112
|
const g = m.identityGateway, https = g.listen.https ?? g.listen.http;
|
|
109
113
|
const ports = [https, ...(g.listen.http && g.listen.http !== https ? [g.listen.http] : [])];
|
|
110
114
|
return { 'nginx-static-config': { enable: [{ type: 'server', relpath: NATIVE_GATEWAY_CONFIG,
|
|
111
|
-
ports: ports.map(port => ({ port, protocol: 'tcp', schema: port === https ? 'https' : 'http' })) }] } };
|
|
115
|
+
...(!m.installPorts ? { ports: ports.map(port => ({ port, protocol: 'tcp', schema: port === https ? 'https' : 'http' })) } : {}) }] } };
|
|
112
116
|
}
|
|
113
117
|
export function renderNativeGatewayConfig(m) {
|
|
114
118
|
if (!nativeGatewaySupported(m))
|
|
@@ -199,3 +203,31 @@ NATIVE_TLS_CONFIG
|
|
|
199
203
|
synopkgland_native_keys || exit $?
|
|
200
204
|
`;
|
|
201
205
|
}
|
|
206
|
+
function managedPortHook(m, hook, wait) {
|
|
207
|
+
const keys = Object.keys(m.identityGateway.listen).map(key => 'gateway_' + key);
|
|
208
|
+
return `synopkgland_managed_port_${wait ? 'wait' : 'diagnostic'}() (
|
|
209
|
+
${wait ? '[ "\${1:-}" = prestart ] || exit 0' : ''}
|
|
210
|
+
target="\${SYNOPKG_PKGDEST:-/var/packages/${m.package}/target}"
|
|
211
|
+
remaining=${wait ? 30 : 0}
|
|
212
|
+
while :; do
|
|
213
|
+
busy=0
|
|
214
|
+
for key in ${keys.join(' ')}; do
|
|
215
|
+
port=
|
|
216
|
+
IFS= read -r port < "$target/install/port-values/$key.value" || [ -n "\${port:-}" ] || exit 0
|
|
217
|
+
case "$port" in ''|*[!0-9]*) exit 0 ;; esac
|
|
218
|
+
[ "$port" -ge 1024 ] && [ "$port" -le 65535 ] || exit 0
|
|
219
|
+
hex=$(printf '%04X' "$port")
|
|
220
|
+
for table in /proc/net/tcp /proc/net/tcp6; do
|
|
221
|
+
[ -r "$table" ] || continue
|
|
222
|
+
if awk -v port="$hex" '$4 == "0A" && toupper($2) ~ (":" port "$") { found=1 } END { exit !found }' "$table"; then busy=1; fi
|
|
223
|
+
done
|
|
224
|
+
${wait ? '' : `logger -p user.warning -t synopkgland "NATIVE_GATEWAY_LIFECYCLE package=${m.package} hook=${hook} port=$port busy=$busy" || :`}
|
|
225
|
+
done
|
|
226
|
+
[ "$busy" -eq 1 ] && [ "$remaining" -gt 0 ] || break
|
|
227
|
+
sleep 0.5 || break
|
|
228
|
+
remaining=$((remaining - 1))
|
|
229
|
+
done
|
|
230
|
+
)
|
|
231
|
+
synopkgland_managed_port_${wait ? 'wait' : 'diagnostic'} "$@" >/dev/null 2>&1 || :
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
@@ -20,6 +20,7 @@ export function renderStandaloneResource(m, portal) {
|
|
|
20
20
|
? renderDockerProjectResource(m.package, "compose", false, m.compose.plan.project_name)
|
|
21
21
|
: {}),
|
|
22
22
|
...(dataShare ? { "data-share": dataShare } : {}),
|
|
23
|
+
...(m.installPorts ? { "port-config": { "protocol-file": `install/${m.package}.sc` } } : {}),
|
|
23
24
|
...(m.capabilities.jobs ? { "systemd-user-unit": {} } : {}),
|
|
24
25
|
...(Object.keys(webConfig).length ? { 'web-config': webConfig } : {}),
|
|
25
26
|
};
|
|
@@ -4,17 +4,17 @@ import { type InstallWizardStep } from "../build-spk/install-wizard.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* `install.sharedFolder`: the Shared Folder a package keeps its files in.
|
|
6
6
|
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `create`:
|
|
11
|
-
*
|
|
7
|
+
* `dedicated`: DSM creates the declared name and grants exactly RO/RW. The
|
|
8
|
+
* wizard may select an existing folder only after checking package access.
|
|
9
|
+
* `select`: an explicit existing folder, with the same field validation.
|
|
10
|
+
* `create`: a fixed DSM resource without an existing-folder option.
|
|
11
|
+
* Removing the package never deletes shared data.
|
|
12
12
|
*/
|
|
13
13
|
export interface SharedFolder {
|
|
14
|
-
mode: "select" | "create";
|
|
14
|
+
mode: "select" | "create" | "dedicated";
|
|
15
15
|
/** The wizard variable and the `var/config.json` entry for this folder. */
|
|
16
16
|
key: string;
|
|
17
|
-
/**
|
|
17
|
+
/** Dedicated or fixed Shared Folder created by DSM data-share. */
|
|
18
18
|
name?: string;
|
|
19
19
|
permission: "ro" | "rw";
|
|
20
20
|
}
|