@synopackageland/cli 0.41.7 → 0.41.8
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 +11 -0
- package/README.md +8 -2
- package/dist/e2e/app-open.d.ts +3 -0
- package/dist/e2e/app-open.js +2 -1
- package/dist/e2e/dsm.d.ts +3 -1
- package/dist/e2e/dsm.js +23 -5
- package/dist/standalone/compose.d.ts +15 -0
- package/dist/standalone/compose.js +14 -5
- package/dist/standalone/manifest.js +10 -7
- package/package.json +1 -1
- package/skills/SKILL.md +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.41.8 (2026-10-30)
|
|
4
|
+
|
|
5
|
+
- Preserve the manifest's installer ports, settings and shared-folder context
|
|
6
|
+
when locking Compose images during build. Apps referencing `install.ports`
|
|
7
|
+
in runtime environment declarations now build as well as pass check.
|
|
8
|
+
- Cover offline locking and Compose scaffold build/inspection with installer
|
|
9
|
+
runtime ports in the CLI release gates.
|
|
10
|
+
- Derive app-open smoke expectations from the manifest: an `admin-only` App
|
|
11
|
+
requires gateway denial for a normal user, with App content and ready markers
|
|
12
|
+
absent. Administrators and permitted normal users still require ready.
|
|
13
|
+
|
|
3
14
|
## 0.41.7 (2026-10-30)
|
|
4
15
|
|
|
5
16
|
- Allow `webapi` raw profiles to set `raw_max_bytes` up to 10 GiB
|
package/README.md
CHANGED
|
@@ -275,8 +275,14 @@ appOpenSmoke({ ready: '[data-app-ready]' });
|
|
|
275
275
|
`ready` is an app-content CSS selector or `{text: 'Exact text'}`. The helper
|
|
276
276
|
reads `dsm-package.yaml` (`app` overrides its filename) and derives the launcher
|
|
277
277
|
and entry using the packaging defaults. `entry` overrides a configured Portal
|
|
278
|
-
alias or ingress URL. Three tests cover administrator (A), normal user
|
|
279
|
-
|
|
278
|
+
alias or ingress URL. Three tests cover administrator (A), normal user (N),
|
|
279
|
+
and a fresh anonymous browser context (X). A must reach `ready`. For
|
|
280
|
+
`entry: admin-only`, N must see the SDK gateway's access-denied page, with
|
|
281
|
+
the `ready` and App content markers absent from the DOM. Set `content` to
|
|
282
|
+
an App shell selector or text marker when it differs from `ready`, for example
|
|
283
|
+
`appOpenSmoke({ready: '[data-app-ready]', content: '#app'})`.
|
|
284
|
+
For other entry kinds, N must reach `ready`; grant that account Application
|
|
285
|
+
Privileges as needed. Keep the App's intended entry policy to run the smoke.
|
|
280
286
|
N explicitly skips when neither `DSM_E2E_ACCOUNT_NORMAL` nor its saved profile
|
|
281
287
|
exists; `normal` selects another named account/profile. A defaults to `admin`.
|
|
282
288
|
An expired saved profile needs its account file to sign in again and fails if
|
package/dist/e2e/app-open.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface AppOpenSmokeOptions {
|
|
|
9
9
|
/** Override the manifest-derived entry, e.g. a configured Portal alias. */
|
|
10
10
|
entry?: string;
|
|
11
11
|
ready: SmokeMarker;
|
|
12
|
+
/** App shell/content marker to exclude on denied pages; defaults to ready. */
|
|
13
|
+
content?: SmokeMarker;
|
|
12
14
|
/** Required only for an entry the manifest declares upstream-authenticated. */
|
|
13
15
|
publicLogin?: SmokeMarker;
|
|
14
16
|
admin?: string;
|
|
@@ -21,6 +23,7 @@ export interface SmokeEntry {
|
|
|
21
23
|
entry: string;
|
|
22
24
|
window: boolean;
|
|
23
25
|
upstreamAuthenticated: boolean;
|
|
26
|
+
normalExpectation: 'ready' | 'denied';
|
|
24
27
|
}
|
|
25
28
|
/** Uses the same launch defaults and legacy web entry resolver as packaging. */
|
|
26
29
|
export declare function deriveSmokeEntry(raw: any, entry?: string): SmokeEntry;
|
package/dist/e2e/app-open.js
CHANGED
|
@@ -36,7 +36,8 @@ export function deriveSmokeEntry(raw, entry) {
|
|
|
36
36
|
const upstreamAuthenticated = (gateway?.upstream_access === 'public' && pathname !== menuEntry) ||
|
|
37
37
|
(!native && raw.integration?.web?.access === 'public') ||
|
|
38
38
|
(gateway?.upstream_auth_paths ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`));
|
|
39
|
-
return { package: raw.package, appId, entry: selected, window: entry ? false : window, upstreamAuthenticated
|
|
39
|
+
return { package: raw.package, appId, entry: selected, window: entry ? false : window, upstreamAuthenticated,
|
|
40
|
+
normalExpectation: raw.entry === 'admin-only' ? 'denied' : 'ready' };
|
|
40
41
|
}
|
|
41
42
|
export function readSmokeEntry(options) {
|
|
42
43
|
return deriveSmokeEntry(parse(readFileSync(options.app ?? 'dsm-package.yaml', 'utf8')), options.entry);
|
package/dist/e2e/dsm.d.ts
CHANGED
|
@@ -193,5 +193,7 @@ export declare function openPortal(app: App, path: string, browser?: DialogBrows
|
|
|
193
193
|
export declare function normalSmokeSkip(user?: string, env?: Env): string | undefined;
|
|
194
194
|
/** Checks rendered errors as well as HTTP errors; ready alone never proves success. */
|
|
195
195
|
export declare function assertSmokePage(page: Page, ready: SmokeMarker, anonymous?: boolean, publicLogin?: SmokeMarker): Promise<void>;
|
|
196
|
-
/**
|
|
196
|
+
/** A denied entry must show the SDK denial page and contain no App markers. */
|
|
197
|
+
export declare function assertSmokeAccess(page: Page, root: Page | Locator, options: Pick<AppOpenSmokeOptions, 'ready' | 'content'>, expectation: 'ready' | 'denied', timeout: number): Promise<void>;
|
|
198
|
+
/** Registers A (admin), N (manifest entry policy), X (fresh anonymous context). */
|
|
197
199
|
export declare function appOpenSmoke(options: AppOpenSmokeOptions): void;
|
package/dist/e2e/dsm.js
CHANGED
|
@@ -587,7 +587,7 @@ export function normalSmokeSkip(user = "normal", env = process.env) {
|
|
|
587
587
|
const file = profilePath(env.DSM_E2E_TARGET || "dsm", user, env);
|
|
588
588
|
if (existsSync(file) && loadProfile(file, dsmUrl(env)))
|
|
589
589
|
return undefined;
|
|
590
|
-
return `Normal-user smoke requires ${key} (mode 600) or a saved ${user} profile
|
|
590
|
+
return `Normal-user smoke requires ${key} (mode 600) or a saved ${user} profile.`;
|
|
591
591
|
}
|
|
592
592
|
function marker(root, value) {
|
|
593
593
|
return typeof value === "string" ? root.locator(value).first()
|
|
@@ -610,6 +610,7 @@ async function dsmLoginVisible(page) {
|
|
|
610
610
|
}
|
|
611
611
|
const signInText = /sign in to DSM|log in to DSM|\u767b\u5165 DSM|\u767b\u5f55 DSM/i;
|
|
612
612
|
const errorText = /(?:^|\n)\s*(?:[45]0[0-9]\b|internal server error|bad gateway|service unavailable|gateway timeout|not found|access denied|you are not authorized)/i;
|
|
613
|
+
const gatewayDenied = '[data-error-code="dsm_admin_required"][data-i18n="gateway.admin_required"], [data-error-code="dsm_app_privilege_denied"][data-i18n="gateway.app_privilege_denied"]';
|
|
613
614
|
/** Checks rendered errors as well as HTTP errors; ready alone never proves success. */
|
|
614
615
|
export async function assertSmokePage(page, ready, anonymous = false, publicLogin) {
|
|
615
616
|
if (await page.locator('body').innerText().then(text => errorText.test(text)))
|
|
@@ -629,7 +630,24 @@ export async function assertSmokePage(page, ready, anonymous = false, publicLogi
|
|
|
629
630
|
throw new Error('Signed-in app smoke reached a login gate.');
|
|
630
631
|
}
|
|
631
632
|
}
|
|
632
|
-
/**
|
|
633
|
+
/** A denied entry must show the SDK denial page and contain no App markers. */
|
|
634
|
+
export async function assertSmokeAccess(page, root, options, expectation, timeout) {
|
|
635
|
+
if (expectation === 'ready') {
|
|
636
|
+
await marker(root, options.ready).waitFor({ state: 'visible', timeout });
|
|
637
|
+
await assertSmokePage(page, options.ready);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
await root.locator(gatewayDenied).first().waitFor({ state: 'visible', timeout });
|
|
641
|
+
if (await dsmLoginVisible(page))
|
|
642
|
+
throw new Error('Denied app smoke reached a login gate.');
|
|
643
|
+
if (await marker(root, options.ready).count() || await marker(root, options.content ?? options.ready).count()) {
|
|
644
|
+
throw new Error('Denied app smoke exposed app content or its ready marker.');
|
|
645
|
+
}
|
|
646
|
+
if (await page.locator('body').innerText().then(text => /(?:^|\n)\s*(?:50[0-9]\b|internal server error|bad gateway|service unavailable|gateway timeout)/i.test(text))) {
|
|
647
|
+
throw new Error('Denied app smoke reached a server error page.');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** Registers A (admin), N (manifest entry policy), X (fresh anonymous context). */
|
|
633
651
|
export function appOpenSmoke(options) {
|
|
634
652
|
const entry = readSmokeEntry(options);
|
|
635
653
|
if (entry.upstreamAuthenticated && !options.publicLogin) {
|
|
@@ -644,7 +662,8 @@ export function appOpenSmoke(options) {
|
|
|
644
662
|
if (normal === admin)
|
|
645
663
|
throw new Error('Normal smoke must use a separate named profile.');
|
|
646
664
|
for (const [role, user] of [['A administrator', admin], ['N normal user', normal]]) {
|
|
647
|
-
|
|
665
|
+
const expectation = user === normal ? entry.normalExpectation : 'ready';
|
|
666
|
+
test(`${entry.package}: app opens — ${role}${expectation === 'denied' ? ' (access denied)' : ''}`, {
|
|
648
667
|
timeout, skip: user === normal ? normalSmokeSkip(normal) : undefined,
|
|
649
668
|
}, async ({ app, browser }) => {
|
|
650
669
|
await signIn(app, user);
|
|
@@ -659,8 +678,7 @@ export function appOpenSmoke(options) {
|
|
|
659
678
|
try {
|
|
660
679
|
const root = entry.window ? await openApp(app, entry.appId, timeout, browser)
|
|
661
680
|
: (await handleDialogs(browser), await openPath(app, new URL(entry.entry, dsmUrl()).href), dsmPage());
|
|
662
|
-
await
|
|
663
|
-
await assertSmokePage(page, options.ready);
|
|
681
|
+
await assertSmokeAccess(page, root, options, expectation, timeout);
|
|
664
682
|
if (failures.length)
|
|
665
683
|
throw new Error(`App smoke received HTTP ${failures.join(', ')}.`);
|
|
666
684
|
}
|
|
@@ -87,6 +87,21 @@ export interface ComposeApp {
|
|
|
87
87
|
plan: ComposePlan;
|
|
88
88
|
}
|
|
89
89
|
export declare const COMPOSE_FIELDS: readonly ["host_udp", "devices", "startup_timeout_seconds", "resource_prefix", "files", "env_files", "profiles", "web", "data", "shares", "network", "services", "helpers", "sdk"];
|
|
90
|
+
/** Keep manifest parsing and image-lock replanning on the same installation context. */
|
|
91
|
+
export declare function composeManifestContext(m: Pick<StandaloneManifest, 'identityGateway' | 'capabilities' | 'sharedFolder' | 'installSettings' | 'installPorts'> & {
|
|
92
|
+
package?: string;
|
|
93
|
+
}): {
|
|
94
|
+
package: string | undefined;
|
|
95
|
+
sdk: string | undefined;
|
|
96
|
+
mode: import("./identity-gateway.js").GatewayMode;
|
|
97
|
+
composeMounts: true | undefined;
|
|
98
|
+
sharedFolder: SharedFolder | undefined;
|
|
99
|
+
installSettings: InstallSettings | undefined;
|
|
100
|
+
installPorts: Record<string, {
|
|
101
|
+
default: number;
|
|
102
|
+
protocol: "tcp";
|
|
103
|
+
}> | undefined;
|
|
104
|
+
};
|
|
90
105
|
export declare function readComposeApp(value: unknown, root: string, arches: NativeArchFamily[], fail: (e: StandaloneError) => void, context?: {
|
|
91
106
|
package?: string;
|
|
92
107
|
sdk?: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { portDefaults } from './install-ports.js';
|
|
1
2
|
import { validComposeAppData } from "../runtime/compose-data.js";
|
|
2
3
|
import { composeDiagnosisMessages, renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
|
|
3
4
|
import { COMPOSE_ERRORS } from "./compose-errors.js";
|
|
@@ -58,6 +59,18 @@ function planner(sdk) {
|
|
|
58
59
|
process.once("exit", () => rmSync(work, { recursive: true, force: true }));
|
|
59
60
|
return out;
|
|
60
61
|
}
|
|
62
|
+
/** Keep manifest parsing and image-lock replanning on the same installation context. */
|
|
63
|
+
export function composeManifestContext(m) {
|
|
64
|
+
return {
|
|
65
|
+
package: m.package,
|
|
66
|
+
sdk: m.identityGateway?.sdk,
|
|
67
|
+
mode: m.identityGateway?.mode ?? 'none',
|
|
68
|
+
composeMounts: m.capabilities.composeMounts,
|
|
69
|
+
sharedFolder: m.sharedFolder,
|
|
70
|
+
installSettings: m.installSettings,
|
|
71
|
+
installPorts: m.installPorts ? portDefaults(m) : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
61
74
|
export function readComposeApp(value, root, arches, fail, context = {}) {
|
|
62
75
|
if (value === undefined)
|
|
63
76
|
return;
|
|
@@ -287,12 +300,8 @@ export function lockComposeImages(m, arch, resolveImages, imageProofs) {
|
|
|
287
300
|
const compose = readComposeApp(m.compose.declaration, m.root, m.arches, (e) => {
|
|
288
301
|
error = e;
|
|
289
302
|
}, {
|
|
290
|
-
|
|
303
|
+
...composeManifestContext(m),
|
|
291
304
|
sdk: m.compose.sdk,
|
|
292
|
-
mode: m.identityGateway?.mode ?? "none",
|
|
293
|
-
composeMounts: m.capabilities.composeMounts,
|
|
294
|
-
sharedFolder: m.sharedFolder,
|
|
295
|
-
installSettings: m.installSettings,
|
|
296
305
|
resolveImages,
|
|
297
306
|
architecture: arch,
|
|
298
307
|
imageProofs,
|
|
@@ -13,7 +13,7 @@ import { parse } from "yaml";
|
|
|
13
13
|
import { isNativeArchFamily, } from "../build-spk/dsm-arch.js";
|
|
14
14
|
import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
|
|
15
15
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
|
|
16
|
-
import { readComposeApp } from "./compose.js";
|
|
16
|
+
import { composeManifestContext, readComposeApp } from "./compose.js";
|
|
17
17
|
import { daemonSdkVersion, readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
18
18
|
import { daemonRestartSdkProblem } from "./daemon-restart.js";
|
|
19
19
|
import { daemonIngressMinDsmProblem } from "./daemon-ingress.js";
|
|
@@ -278,15 +278,18 @@ function readManifest(raw, c) {
|
|
|
278
278
|
const installSettings = readInstallSettings(isRecord(raw.install) ? raw.install.settings : undefined, e => c.fail(e.code, e.message));
|
|
279
279
|
const hyperBackup = readHyperBackup(raw.backup, { compose: raw.compose !== undefined }, e => c.fail(e.code, e.message));
|
|
280
280
|
const compose = arches &&
|
|
281
|
-
readComposeApp(raw.compose, c.root, arches, (e) => c.fail(e.code, e.message), {
|
|
281
|
+
readComposeApp(raw.compose, c.root, arches, (e) => c.fail(e.code, e.message), composeManifestContext({
|
|
282
282
|
package: packageId,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
identityGateway: {
|
|
284
|
+
...gatewayRaw,
|
|
285
|
+
sdk: typeof gatewayRaw.sdk === "string" ? gatewayRaw.sdk : undefined,
|
|
286
|
+
mode: typeof gatewayRaw.mode === "string" ? gatewayRaw.mode : "none",
|
|
287
|
+
},
|
|
288
|
+
capabilities,
|
|
286
289
|
sharedFolder,
|
|
287
290
|
installSettings,
|
|
288
|
-
installPorts
|
|
289
|
-
});
|
|
291
|
+
installPorts,
|
|
292
|
+
}));
|
|
290
293
|
if (raw.compose !== undefined &&
|
|
291
294
|
((raw.daemon !== undefined && !timeoutOnly) ||
|
|
292
295
|
Object.keys(capabilities).some((k) => k !== "composeMounts") ||
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -316,7 +316,11 @@ Use `appOpenSmoke` from `@synopackageland/cli/e2e`; keep one content-specific
|
|
|
316
316
|
reuses the existing DSM credentials/profiles/tunnel, and registers administrator,
|
|
317
317
|
normal-user and fresh anonymous tests. Configure `DSM_E2E_ACCOUNT_ADMIN` and
|
|
318
318
|
`DSM_E2E_ACCOUNT_NORMAL` as private account files; a missing normal profile and
|
|
319
|
-
credentials is a reported skip.
|
|
319
|
+
credentials is a reported skip. For `entry: admin-only`, N must see the SDK
|
|
320
|
+
gateway denial page with no App content or ready marker in the DOM; use
|
|
321
|
+
`content` for a separate App shell marker. Other entry kinds require N to
|
|
322
|
+
reach `ready`; grant Application Privileges as needed. Preserve the manifest's
|
|
323
|
+
entry policy when running the smoke.
|
|
320
324
|
Override `app` for a manifest filename or `entry` for a configured URL.
|
|
321
325
|
Only a manifest-declared upstream-authenticated entry may use `publicLogin`.
|
|
322
326
|
Keep `.env` and `.e2e/` untracked; never add NAS details to source.
|