@synopackageland/cli 0.17.5 → 0.17.7
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
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.7 (2026-10-20)
|
|
4
|
+
|
|
5
|
+
- Inspect Compose Apps with custom icons and localized titles using their archived build inputs, preserving every icon size and strict launcher validation. Preserve declared daemon uninstall hooks when checking fixed lifecycle scripts.
|
|
6
|
+
|
|
7
|
+
## 0.17.6 (2026-10-20)
|
|
8
|
+
|
|
9
|
+
- Inspect sharedFolder, Portal and install-selected Apps by reconstructing fixed lifecycle scripts from their complete inputs, including native HTTPS ingress and App hooks.
|
|
10
|
+
|
|
3
11
|
## 0.17.5 (2026-10-20)
|
|
4
12
|
|
|
5
13
|
- Run native HTTPS preparation inside the SDK lifecycle steps before App hooks, keeping generated hook wrappers inspectable.
|
|
@@ -34,5 +34,7 @@ export declare function iconReport(root: string, icon: string | undefined): Icon
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function windowIconTemplate(icons: PackageIcons): string;
|
|
36
36
|
export declare function windowIconFile(icons: PackageIcons, size: number): string;
|
|
37
|
+
/** Recover the build's exact icon inputs from an SPK, including hand-drawn sizes. */
|
|
38
|
+
export declare function readDsmWindowIcons(uiDir: string): PackageIcons;
|
|
37
39
|
export declare function writeDsmWindowIcons(uiDir: string, icons: PackageIcons): void;
|
|
38
40
|
export declare function writePackageCenterIcons(spkRoot: string, icons: PackageIcons): void;
|
|
@@ -149,6 +149,36 @@ export function windowIconTemplate(icons) {
|
|
|
149
149
|
export function windowIconFile(icons, size) {
|
|
150
150
|
return windowIconTemplate(icons).replace("{0}", String(size));
|
|
151
151
|
}
|
|
152
|
+
/** Recover the build's exact icon inputs from an SPK, including hand-drawn sizes. */
|
|
153
|
+
export function readDsmWindowIcons(uiDir) {
|
|
154
|
+
const dir = join(uiDir, "images");
|
|
155
|
+
const candidates = readdirSync(dir).filter(name => /^app_[a-f0-9]{12}_256\.png$/.test(name));
|
|
156
|
+
if (candidates.length !== 1)
|
|
157
|
+
throw new Error("expected one complete DSM window icon set");
|
|
158
|
+
const template = candidates[0].replace(/_256\.png$/, "_{0}.png");
|
|
159
|
+
const window = new Map();
|
|
160
|
+
let solid = false;
|
|
161
|
+
for (const size of WINDOW_ICON_SIZES) {
|
|
162
|
+
const name = template.replace("{0}", String(size));
|
|
163
|
+
const bytes = readFileSync(join(dir, name));
|
|
164
|
+
if (bytes.length > MAX_ICON_BYTES)
|
|
165
|
+
throw new Error(`${name}: invalid DSM window icon`);
|
|
166
|
+
const image = decodePng(bytes);
|
|
167
|
+
if (image.width !== size || image.height !== size)
|
|
168
|
+
throw new Error(`${name}: invalid DSM window icon`);
|
|
169
|
+
if (size === 256)
|
|
170
|
+
solid = isSolid(image);
|
|
171
|
+
window.set(size, bytes);
|
|
172
|
+
}
|
|
173
|
+
const icons = {
|
|
174
|
+
source: window.get(256).equals(defaultPackageIcons().packageIcon256) ? "default" : "custom",
|
|
175
|
+
window, solid,
|
|
176
|
+
packageIcon: window.get(64), packageIcon256: window.get(256),
|
|
177
|
+
};
|
|
178
|
+
if (windowIconTemplate(icons) !== `images/${template}`)
|
|
179
|
+
throw new Error("DSM window icon filenames differ from their content hash");
|
|
180
|
+
return icons;
|
|
181
|
+
}
|
|
152
182
|
export function writeDsmWindowIcons(uiDir, icons) {
|
|
153
183
|
mkdirSync(join(uiDir, "images"), { recursive: true });
|
|
154
184
|
for (const [size, png] of icons.window)
|
|
@@ -24,11 +24,12 @@ import { daemonConfig, readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
|
24
24
|
import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
|
|
25
25
|
import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
|
|
26
26
|
import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
|
|
27
|
+
import { DSM_LANGS } from "./i18n.js";
|
|
27
28
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, BRIDGE_WORKER_ARGS, backgroundUnitName, renderBackgroundUnit, } from "./background.js";
|
|
28
29
|
import { renderStandaloneResource } from "./resource.js";
|
|
29
30
|
import { recoverSharedFolder, renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps, SHARED_FOLDER_MARKER, } from "./shared-folder.js";
|
|
30
31
|
import { renderInstallWizardFiles, renderNativeInstallWizard, } from "../build-spk/install-wizard.js";
|
|
31
|
-
import { renderDelegateScopeSteps,
|
|
32
|
+
import { renderDelegateScopeSteps, } from "../build-spk/delegate-scope.js";
|
|
32
33
|
import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
|
|
33
34
|
import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
34
35
|
import { renderUiConfig } from "./dsm-window.js";
|
|
@@ -39,7 +40,7 @@ import { listPortExposures, loadCompose } from "../compose/analyze.js";
|
|
|
39
40
|
import { uninstallCleanup, } from "../runtime/uninstall-cleanup.js";
|
|
40
41
|
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep, } from "../portal/package.js";
|
|
41
42
|
import { isPortalAlias } from "../portal/alias.js";
|
|
42
|
-
import { defaultPackageIcons, MAX_ICON_BYTES } from "../build-spk/dsm-icons.js";
|
|
43
|
+
import { defaultPackageIcons, MAX_ICON_BYTES, readDsmWindowIcons } from "../build-spk/dsm-icons.js";
|
|
43
44
|
import { isDefaultLoginAsset } from "../build-spk/login-style.js";
|
|
44
45
|
import { decodePng } from "../build-spk/png.js";
|
|
45
46
|
/** The SPK kind of each sdk-package.json backend a CGI is built for: Go itself, or the runtime of an App in another language. */
|
|
@@ -162,7 +163,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
162
163
|
: undefined;
|
|
163
164
|
// Resource-worker portals use DSM-owned INFO reload, with no executor requirement.
|
|
164
165
|
const portal = standalone
|
|
165
|
-
? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings)
|
|
166
|
+
? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings, nativeGateway)
|
|
166
167
|
: undefined;
|
|
167
168
|
if (portal &&
|
|
168
169
|
info.os_min_ver &&
|
|
@@ -198,7 +199,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
198
199
|
if (standalone && portal)
|
|
199
200
|
findings.push(...inspectPortalInfo(info, hasExecutor));
|
|
200
201
|
const daemon = standalone
|
|
201
|
-
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway)
|
|
202
|
+
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall)
|
|
202
203
|
: undefined;
|
|
203
204
|
if (background)
|
|
204
205
|
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts);
|
|
@@ -269,7 +270,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
269
270
|
? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
|
|
270
271
|
: undefined;
|
|
271
272
|
if (standalone)
|
|
272
|
-
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor);
|
|
273
|
+
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor, nativeGateway, info.os_min_ver);
|
|
273
274
|
const icon = inspectPackageIcons(dir, findings);
|
|
274
275
|
const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
|
|
275
276
|
if (standalone && !cgiEntry) {
|
|
@@ -627,6 +628,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
627
628
|
displayname: info.displayname,
|
|
628
629
|
description: info.description,
|
|
629
630
|
maintainer: info.maintainer,
|
|
631
|
+
i18n: { info: archivedInfoTranslations(info) },
|
|
630
632
|
os_min_ver: info.os_min_ver,
|
|
631
633
|
build: Number(info.version?.split("-").pop()),
|
|
632
634
|
arches: [arch],
|
|
@@ -653,7 +655,10 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
653
655
|
throw new Error("unexpected worker resource; unapproved Shares must never auto-acquire");
|
|
654
656
|
if (readdirSync(join(dir, "conf")).some((n) => !["privilege", ...(resource ? ["resource"] : [])].includes(n)))
|
|
655
657
|
throw new Error("unexpected Compose resource");
|
|
656
|
-
|
|
658
|
+
// Replay build's shared renderer with the archived bytes, never a project
|
|
659
|
+
// path or the SDK default. Each hand-drawn icon size affects the URL hash.
|
|
660
|
+
const icons = readDsmWindowIcons(join(root, "ui"));
|
|
661
|
+
if (readOptional(join(root, "ui/config")) !== renderUiConfig(manifest, icons))
|
|
657
662
|
throw new Error("LAUNCHER_TYPE_MISMATCH classless Compose must use the shared url launcher");
|
|
658
663
|
if (g &&
|
|
659
664
|
(cfg.package !== info.package ||
|
|
@@ -787,7 +792,7 @@ async function recoverCompose(dir, value, findings) {
|
|
|
787
792
|
return undefined;
|
|
788
793
|
}
|
|
789
794
|
}
|
|
790
|
-
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway) {
|
|
795
|
+
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway, uninstall) {
|
|
791
796
|
if (![...paths].some((path) => path.startsWith("daemon/")))
|
|
792
797
|
return;
|
|
793
798
|
if (info.precheckstartstop !== "yes")
|
|
@@ -843,6 +848,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
843
848
|
root: dir,
|
|
844
849
|
...(combined ? { lifecycleTimeouts: daemon.timeouts } : {}),
|
|
845
850
|
daemon,
|
|
851
|
+
uninstall,
|
|
846
852
|
...combined,
|
|
847
853
|
capabilities: {
|
|
848
854
|
...(executor ? { rootOps: true } : {}),
|
|
@@ -879,6 +885,22 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
879
885
|
findings.push("daemon/config.json or the payload does not match the fixed daemon contract");
|
|
880
886
|
}
|
|
881
887
|
}
|
|
888
|
+
/** INFO retains the localized title inputs used by build's ui/config renderer. */
|
|
889
|
+
function archivedInfoTranslations(info) {
|
|
890
|
+
const translations = {};
|
|
891
|
+
for (const [locale, lang] of Object.entries(DSM_LANGS)) {
|
|
892
|
+
if (locale === "en")
|
|
893
|
+
continue;
|
|
894
|
+
const displayname = info[`displayname_${lang}`];
|
|
895
|
+
const description = info[`description_${lang}`];
|
|
896
|
+
if (displayname || description)
|
|
897
|
+
translations[locale] = {
|
|
898
|
+
...(displayname ? { displayname } : {}),
|
|
899
|
+
...(description ? { description } : {}),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
return translations;
|
|
903
|
+
}
|
|
882
904
|
function readInfo(path, findings, kind, compose = false) {
|
|
883
905
|
if (!existsSync(path)) {
|
|
884
906
|
findings.push("INFO is missing");
|
|
@@ -1081,7 +1103,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
|
|
|
1081
1103
|
* declaration (without one, any present file must be the default native
|
|
1082
1104
|
* install wizard), and no other install/upgrade wizard file may exist.
|
|
1083
1105
|
*/
|
|
1084
|
-
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false) {
|
|
1106
|
+
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false, nativeGateway, osMinVer) {
|
|
1085
1107
|
const wizardDir = join(dir, "WIZARD_UIFILES");
|
|
1086
1108
|
const present = (existsSync(wizardDir) ? readdirSync(wizardDir) : [])
|
|
1087
1109
|
.filter((name) => /^(install|upgrade)_uifile(_|\.|$)/.test(name))
|
|
@@ -1091,6 +1113,22 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1091
1113
|
findings.push("the install.sharedFolder declaration in scripts/postinst is invalid");
|
|
1092
1114
|
if (!pkg)
|
|
1093
1115
|
return;
|
|
1116
|
+
// App wrappers have already been validated and unwrapped. Reconstruct the
|
|
1117
|
+
// remaining install-hook inputs from INFO, the payload and its build record.
|
|
1118
|
+
const lifecycleManifest = {
|
|
1119
|
+
package: pkg,
|
|
1120
|
+
os_min_ver: osMinVer,
|
|
1121
|
+
sharedFolder: folder,
|
|
1122
|
+
capabilities: selected
|
|
1123
|
+
? { delegate: { scope: "install-selected" } }
|
|
1124
|
+
: executor
|
|
1125
|
+
? { rootOps: true }
|
|
1126
|
+
: {},
|
|
1127
|
+
...(nativeGateway
|
|
1128
|
+
? { identityGateway: nativeGateway }
|
|
1129
|
+
: {}),
|
|
1130
|
+
...portalManifest(portal),
|
|
1131
|
+
};
|
|
1094
1132
|
const wizards = expectedWizardFiles(pkg, folder, selected, portal);
|
|
1095
1133
|
const source = selected
|
|
1096
1134
|
? "the install-selected policy"
|
|
@@ -1110,25 +1148,17 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1110
1148
|
findings.push(`WIZARD_UIFILES/${name} is not a wizard file produced by ${source}`);
|
|
1111
1149
|
if (selected) {
|
|
1112
1150
|
for (const hook of ["preinst", "postinst"]) {
|
|
1113
|
-
if (
|
|
1114
|
-
|
|
1151
|
+
if (readOptional(join(dir, "scripts", hook)) !==
|
|
1152
|
+
renderStandaloneScript(hook, lifecycleManifest))
|
|
1153
|
+
findings.push(`scripts/${hook} does not match the fixed install-selected draft handling`);
|
|
1115
1154
|
}
|
|
1116
1155
|
}
|
|
1117
|
-
if (!folder)
|
|
1156
|
+
if (!folder && !portal)
|
|
1118
1157
|
return;
|
|
1119
1158
|
for (const hook of ["postinst", "postupgrade"]) {
|
|
1120
1159
|
if (readOptional(join(dir, "scripts", hook)) !==
|
|
1121
|
-
renderStandaloneScript(hook, {
|
|
1122
|
-
|
|
1123
|
-
sharedFolder: folder,
|
|
1124
|
-
...(selected
|
|
1125
|
-
? { capabilities: { delegate: { scope: "install-selected" } } }
|
|
1126
|
-
: executor
|
|
1127
|
-
? { capabilities: { rootOps: true } }
|
|
1128
|
-
: {}),
|
|
1129
|
-
...portalManifest(portal),
|
|
1130
|
-
})) {
|
|
1131
|
-
findings.push(`scripts/${hook} does not match the fixed script declared by install.sharedFolder`);
|
|
1160
|
+
renderStandaloneScript(hook, lifecycleManifest)) {
|
|
1161
|
+
findings.push(`scripts/${hook} does not match the fixed script declared by ${folder ? "install.sharedFolder" : "portal"}`);
|
|
1132
1162
|
}
|
|
1133
1163
|
}
|
|
1134
1164
|
}
|
|
@@ -1160,7 +1190,7 @@ function portalManifest(portal) {
|
|
|
1160
1190
|
* files and alias lifecycle must be exactly what the generator writes for the
|
|
1161
1191
|
* package's own DSM window (native AppInstance or Compose `.url` entry); activation is never part of the package.
|
|
1162
1192
|
*/
|
|
1163
|
-
async function recoverPortal(dir, info, appId, paths, payload, findings) {
|
|
1193
|
+
async function recoverPortal(dir, info, appId, paths, payload, findings, nativeGateway) {
|
|
1164
1194
|
const files = [PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH];
|
|
1165
1195
|
if (!files.some((path) => paths.has(path)))
|
|
1166
1196
|
return;
|
|
@@ -1184,7 +1214,14 @@ async function recoverPortal(dir, info, appId, paths, payload, findings) {
|
|
|
1184
1214
|
renderPortalMustache())
|
|
1185
1215
|
throw new Error("content");
|
|
1186
1216
|
for (const hook of ["preinst", "postinst", "postupgrade"]) {
|
|
1187
|
-
|
|
1217
|
+
const prepare = ["postinst", "postupgrade"].includes(hook)
|
|
1218
|
+
? renderNativeGatewayPrepare({
|
|
1219
|
+
package: info.package,
|
|
1220
|
+
os_min_ver: info.os_min_ver,
|
|
1221
|
+
identityGateway: nativeGateway,
|
|
1222
|
+
})
|
|
1223
|
+
: "";
|
|
1224
|
+
if (!readOptional(join(dir, "scripts", hook))?.startsWith(`#!/bin/sh\n${prepare}${renderPortalLifecycle(hook, portal)}`))
|
|
1188
1225
|
throw new Error(hook);
|
|
1189
1226
|
}
|
|
1190
1227
|
return portal;
|