@synopackageland/cli 0.41.7 → 0.41.9

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.41.9 (2026-10-30)
4
+
5
+ - Accept self-signed DSM and redirected gateway certificates by default in the
6
+ Chromium e2e target, before its first network navigation.
7
+ - Preserve installer-managed ports when the SPK inspector reconstructs daemon
8
+ and background lifecycle hooks, so native gateway Apps with `install.ports`
9
+ build and inspect consistently.
10
+
11
+ ## 0.41.8 (2026-10-30)
12
+
13
+ - Preserve the manifest's installer ports, settings and shared-folder context
14
+ when locking Compose images during build. Apps referencing `install.ports`
15
+ in runtime environment declarations now build as well as pass check.
16
+ - Cover offline locking and Compose scaffold build/inspection with installer
17
+ runtime ports in the CLI release gates.
18
+ - Derive app-open smoke expectations from the manifest: an `admin-only` App
19
+ requires gateway denial for a normal user, with App content and ready markers
20
+ absent. Administrators and permitted normal users still require ready.
21
+
3
22
  ## 0.41.7 (2026-10-30)
4
23
 
5
24
  - Allow `webapi` raw profiles to set `raw_max_bytes` up to 10 GiB
package/README.md CHANGED
@@ -225,6 +225,10 @@ not change how the App runs or its privilege tier. See `synopkgland skill show b
225
225
  suite on [`e2e`](https://www.npmjs.com/package/e2e) and `@e2e-dev/web`
226
226
  (optional peers, with `ai` and `@ai-sdk/openai`).
227
227
 
228
+ `dsmTarget()` uses Chromium and accepts self-signed DSM and gateway HTTPS
229
+ certificates by default, including redirects from an HTTP DSM URL. It configures
230
+ CDP before the first network navigation; this certificate handling is Chromium-only.
231
+
228
232
  ```ts
229
233
  // e2e.config.ts
230
234
  import {dsmTarget, magpieAgent} from '@synopackageland/cli/e2e';
@@ -275,8 +279,14 @@ appOpenSmoke({ ready: '[data-app-ready]' });
275
279
  `ready` is an app-content CSS selector or `{text: 'Exact text'}`. The helper
276
280
  reads `dsm-package.yaml` (`app` overrides its filename) and derives the launcher
277
281
  and entry using the packaging defaults. `entry` overrides a configured Portal
278
- alias or ingress URL. Three tests cover administrator (A), normal user with
279
- Application Privileges (N), and a fresh anonymous browser context (X).
282
+ alias or ingress URL. Three tests cover administrator (A), normal user (N),
283
+ and a fresh anonymous browser context (X). A must reach `ready`. For
284
+ `entry: admin-only`, N must see the SDK gateway's access-denied page, with
285
+ the `ready` and App content markers absent from the DOM. Set `content` to
286
+ an App shell selector or text marker when it differs from `ready`, for example
287
+ `appOpenSmoke({ready: '[data-app-ready]', content: '#app'})`.
288
+ For other entry kinds, N must reach `ready`; grant that account Application
289
+ Privileges as needed. Keep the App's intended entry policy to run the smoke.
280
290
  N explicitly skips when neither `DSM_E2E_ACCOUNT_NORMAL` nor its saved profile
281
291
  exists; `normal` selects another named account/profile. A defaults to `admin`.
282
292
  An expired saved profile needs its account file to sign in again and fails if
@@ -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;
@@ -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);
@@ -0,0 +1,6 @@
1
+ import { type EngineHandle } from "e2e/engine";
2
+ import type { surfaceOf } from "@e2e-dev/web";
3
+ type Surface = NonNullable<ReturnType<typeof surfaceOf>>;
4
+ /** Chromium DSM test targets accept self-signed DSM and redirected gateway certificates. */
5
+ export declare function withDsmCertificates(base: EngineHandle, surface: () => Surface): EngineHandle;
6
+ export {};
@@ -0,0 +1,60 @@
1
+ import { defineEngine } from "e2e/engine";
2
+ /** Chromium DSM test targets accept self-signed DSM and redirected gateway certificates. */
3
+ export function withDsmCertificates(base, surface) {
4
+ const browsers = new WeakMap();
5
+ const pages = new WeakMap();
6
+ async function prepareBrowser() {
7
+ const browser = surface().context().browser();
8
+ if (!browser)
9
+ return false;
10
+ let pending = browsers.get(browser);
11
+ if (!pending) {
12
+ pending = (async () => {
13
+ const session = await browser.newBrowserCDPSession();
14
+ await session.send("Security.setIgnoreCertificateErrors", { ignore: true });
15
+ return true;
16
+ })().catch(() => false); // Some Chromium connections expose only page CDP.
17
+ browsers.set(browser, pending);
18
+ }
19
+ return pending;
20
+ }
21
+ const { capabilities: _capabilities, ...spec } = base;
22
+ return defineEngine({
23
+ ...spec,
24
+ async startAttempt(context) {
25
+ await base.startAttempt?.(context);
26
+ await prepareBrowser();
27
+ },
28
+ session: {
29
+ ...base.session,
30
+ async open(url, operation) {
31
+ if (!base.session?.open)
32
+ throw new Error("The DSM engine cannot open a page.");
33
+ if (!await prepareBrowser()) {
34
+ // The runner creates its page lazily. Open only a blank page before
35
+ // configuring page CDP, so the first network navigation is covered.
36
+ let page;
37
+ try {
38
+ page = surface().page();
39
+ }
40
+ catch {
41
+ await base.session.open("about:blank", operation);
42
+ page = surface().page();
43
+ }
44
+ let pending = pages.get(page);
45
+ if (!pending) {
46
+ const target = page;
47
+ pending = (async () => {
48
+ const session = await surface().context().newCDPSession(target);
49
+ await session.send("Security.setIgnoreCertificateErrors", { ignore: true });
50
+ })();
51
+ pages.set(page, pending);
52
+ pending.catch(() => pages.delete(target));
53
+ }
54
+ await pending;
55
+ }
56
+ await base.session.open(url, operation);
57
+ },
58
+ },
59
+ });
60
+ }
package/dist/e2e/dsm.d.ts CHANGED
@@ -82,7 +82,7 @@ export declare function loadProfile(file: string, url: string): StorageState | n
82
82
  * process, each write their own temporary file and rename it into place.
83
83
  */
84
84
  export declare function saveProfile(file: string, url: string, state: StorageState): void;
85
- /** The `targets` entry of e2e.config.ts. */
85
+ /** The Chromium `targets` entry of e2e.config.ts; accepts self-signed test certificates. */
86
86
  export declare function dsmTarget(name?: string): {
87
87
  name: string;
88
88
  engine: import("e2e/engine").EngineHandle;
@@ -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
- /** Registers A (admin), N (permitted normal user), X (fresh anonymous context). */
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
@@ -23,6 +23,7 @@ import { createOpenAI } from "@ai-sdk/openai";
23
23
  import { readSmokeEntry } from "./app-open.js";
24
24
  export { deriveSmokeEntry } from "./app-open.js";
25
25
  import { surfaceOf, test, web } from "@e2e-dev/web";
26
+ import { withDsmCertificates } from "./certificates.js";
26
27
  const safeName = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
27
28
  const desktopSelector = "#sds-desktop";
28
29
  const signedInSelector = "#sds-taskbar-tray-item-user";
@@ -288,15 +289,16 @@ export function saveProfile(file, url, state) {
288
289
  renameSync(temporary, file);
289
290
  }
290
291
  // ---- Browser --------------------------------------------------------------
291
- const engine = web({
292
+ const webEngine = web({
292
293
  viewport: { width: Number(process.env.DSM_E2E_VIEWPORT_WIDTH || 1280), height: Number(process.env.DSM_E2E_VIEWPORT_HEIGHT || 800) },
293
294
  });
294
- /** The `targets` entry of e2e.config.ts. */
295
+ const engine = withDsmCertificates(webEngine, surface);
296
+ /** The Chromium `targets` entry of e2e.config.ts; accepts self-signed test certificates. */
295
297
  export function dsmTarget(name = "dsm") {
296
298
  return { name, engine, app: { url: dsmUrl() } };
297
299
  }
298
300
  function surface() {
299
- const live = surfaceOf(engine);
301
+ const live = surfaceOf(webEngine);
300
302
  if (!live)
301
303
  throw new Error("The DSM web engine is unavailable.");
302
304
  return live;
@@ -587,7 +589,7 @@ export function normalSmokeSkip(user = "normal", env = process.env) {
587
589
  const file = profilePath(env.DSM_E2E_TARGET || "dsm", user, env);
588
590
  if (existsSync(file) && loadProfile(file, dsmUrl(env)))
589
591
  return undefined;
590
- return `Normal-user smoke requires ${key} (mode 600) or a saved ${user} profile with app permission.`;
592
+ return `Normal-user smoke requires ${key} (mode 600) or a saved ${user} profile.`;
591
593
  }
592
594
  function marker(root, value) {
593
595
  return typeof value === "string" ? root.locator(value).first()
@@ -610,6 +612,7 @@ async function dsmLoginVisible(page) {
610
612
  }
611
613
  const signInText = /sign in to DSM|log in to DSM|\u767b\u5165 DSM|\u767b\u5f55 DSM/i;
612
614
  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;
615
+ 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
616
  /** Checks rendered errors as well as HTTP errors; ready alone never proves success. */
614
617
  export async function assertSmokePage(page, ready, anonymous = false, publicLogin) {
615
618
  if (await page.locator('body').innerText().then(text => errorText.test(text)))
@@ -629,7 +632,24 @@ export async function assertSmokePage(page, ready, anonymous = false, publicLogi
629
632
  throw new Error('Signed-in app smoke reached a login gate.');
630
633
  }
631
634
  }
632
- /** Registers A (admin), N (permitted normal user), X (fresh anonymous context). */
635
+ /** A denied entry must show the SDK denial page and contain no App markers. */
636
+ export async function assertSmokeAccess(page, root, options, expectation, timeout) {
637
+ if (expectation === 'ready') {
638
+ await marker(root, options.ready).waitFor({ state: 'visible', timeout });
639
+ await assertSmokePage(page, options.ready);
640
+ return;
641
+ }
642
+ await root.locator(gatewayDenied).first().waitFor({ state: 'visible', timeout });
643
+ if (await dsmLoginVisible(page))
644
+ throw new Error('Denied app smoke reached a login gate.');
645
+ if (await marker(root, options.ready).count() || await marker(root, options.content ?? options.ready).count()) {
646
+ throw new Error('Denied app smoke exposed app content or its ready marker.');
647
+ }
648
+ 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))) {
649
+ throw new Error('Denied app smoke reached a server error page.');
650
+ }
651
+ }
652
+ /** Registers A (admin), N (manifest entry policy), X (fresh anonymous context). */
633
653
  export function appOpenSmoke(options) {
634
654
  const entry = readSmokeEntry(options);
635
655
  if (entry.upstreamAuthenticated && !options.publicLogin) {
@@ -644,7 +664,8 @@ export function appOpenSmoke(options) {
644
664
  if (normal === admin)
645
665
  throw new Error('Normal smoke must use a separate named profile.');
646
666
  for (const [role, user] of [['A administrator', admin], ['N normal user', normal]]) {
647
- test(`${entry.package}: app opens — ${role}`, {
667
+ const expectation = user === normal ? entry.normalExpectation : 'ready';
668
+ test(`${entry.package}: app opens — ${role}${expectation === 'denied' ? ' (access denied)' : ''}`, {
648
669
  timeout, skip: user === normal ? normalSmokeSkip(normal) : undefined,
649
670
  }, async ({ app, browser }) => {
650
671
  await signIn(app, user);
@@ -659,8 +680,7 @@ export function appOpenSmoke(options) {
659
680
  try {
660
681
  const root = entry.window ? await openApp(app, entry.appId, timeout, browser)
661
682
  : (await handleDialogs(browser), await openPath(app, new URL(entry.entry, dsmUrl()).href), dsmPage());
662
- await marker(root, options.ready).waitFor({ state: 'visible', timeout });
663
- await assertSmokePage(page, options.ready);
683
+ await assertSmokeAccess(page, root, options, expectation, timeout);
664
684
  if (failures.length)
665
685
  throw new Error(`App smoke received HTTP ${failures.join(', ')}.`);
666
686
  }
@@ -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
- package: m.package,
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,
@@ -248,10 +248,10 @@ export async function inspectSpk(spkPath, expectedKind) {
248
248
  if (standalone && portal)
249
249
  findings.push(...inspectPortalInfo(info, hasExecutor));
250
250
  const daemon = standalone
251
- ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall, { ingress: daemonIngress, node: daemonNode, restart: daemonRestart })
251
+ ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall, { ingress: daemonIngress, node: daemonNode, restart: daemonRestart, installPorts: Boolean(metadata.installPorts) })
252
252
  : undefined;
253
253
  if (background)
254
- checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts, nativeGateway);
254
+ checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts, nativeGateway, Boolean(metadata.installPorts));
255
255
  let executor;
256
256
  if ((metadata.capabilities ?? []).some((name) => name === "rootOps" || name === "delegate" || name === "webapi") ||
257
257
  paths.has("executor/executor")) {
@@ -1026,6 +1026,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
1026
1026
  throw new Error("missing executable");
1027
1027
  const manifest = {
1028
1028
  package: info.package,
1029
+ ...(extras.installPorts ? { installPorts: true } : {}),
1029
1030
  backend: { kind: 'go', dir: dir, main: './cmd/app', timeoutSeconds: config.operation_seconds ?? 60 },
1030
1031
  root: dir,
1031
1032
  ...(combined ? { lifecycleTimeouts: daemon.timeouts } : {}),
@@ -1057,7 +1058,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
1057
1058
  if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== JSON.stringify(expectedConfig) + '\n')
1058
1059
  throw new Error("non-canonical daemon config");
1059
1060
  for (const hook of ["start-stop-status", "preupgrade", "preuninst"]) {
1060
- if (!matchesStandaloneScript(hook, nativeGateway ? { ...manifest, os_min_ver: info.os_min_ver, identityGateway: nativeGateway } : manifest, readFileSync(join(dir, "scripts", hook), "utf8")))
1061
+ if (!matchesStandaloneScript(hook, nativeGateway ? { ...manifest, os_min_ver: info.os_min_ver, identityGateway: nativeGateway } : manifest, readFileSync(join(dir, "scripts", hook), "utf8"), Boolean(extras.installPorts)))
1061
1062
  findings.push(`daemon scripts/${hook} does not match the fixed lifecycle`);
1062
1063
  }
1063
1064
  return daemon;
@@ -1379,7 +1380,7 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, c
1379
1380
  }
1380
1381
  }
1381
1382
  /** The opt-in package user unit must be exactly what the generator writes. */
1382
- function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false, lifecycleTimeouts, nativeGateway) {
1383
+ function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false, lifecycleTimeouts, nativeGateway, installPorts = false) {
1383
1384
  const pkg = info.package;
1384
1385
  if (!pkg)
1385
1386
  return;
@@ -1402,6 +1403,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
1402
1403
  }
1403
1404
  if (!matchesStandaloneScript("start-stop-status", {
1404
1405
  package: pkg,
1406
+ ...(installPorts ? { installPorts: true } : {}),
1405
1407
  os_min_ver: info.os_min_ver,
1406
1408
  ...(nativeGateway ? { identityGateway: nativeGateway } : {}),
1407
1409
  lifecycleTimeouts,
@@ -1410,7 +1412,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
1410
1412
  jobs: { worker: "sha256-v1" },
1411
1413
  ...(executor ? { rootOps: true } : {}),
1412
1414
  },
1413
- }, read("scripts/start-stop-status")))
1415
+ }, read("scripts/start-stop-status"), installPorts))
1414
1416
  findings.push("background scripts/start-stop-status does not match the fixed synosystemctl lifecycle");
1415
1417
  }
1416
1418
  /**
@@ -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
- sdk: typeof gatewayRaw.sdk === "string" ? gatewayRaw.sdk : undefined,
284
- mode: typeof gatewayRaw.mode === "string" ? gatewayRaw.mode : "none",
285
- composeMounts: capabilities.composeMounts,
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: installPorts ? portDefaults({ installPorts, identityGateway: gatewayRaw }) : undefined,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.41.7",
3
+ "version": "0.41.9",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
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. Grant Application Privileges before running.
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.