@synopackageland/cli 0.41.6 → 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 CHANGED
@@ -1,5 +1,24 @@
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
+
14
+ ## 0.41.7 (2026-10-30)
15
+
16
+ - Allow `webapi` raw profiles to set `raw_max_bytes` up to 10 GiB
17
+ (10737418240 bytes), matching SDK/App 0.38.3 and executor/v0.38.3. The limit
18
+ shipped in 0.41.6 without a changelog entry. Larger raw downloads need SDK
19
+ 0.38.3; templates ~0.35.5 still pin SDK 0.38.2. Raising a profile limit
20
+ requires administrator approval and fresh user consent.
21
+
3
22
  ## 0.41.6
4
23
 
5
24
  - Share administrator, permitted normal-user and fresh anonymous app-open smoke tests through `@synopackageland/cli/e2e`, with manifest-derived entry URLs and explicit missing-account skips.
package/README.md CHANGED
@@ -88,7 +88,7 @@ alongside npm lockfiles. Do not disable checksum verification for public SDK
88
88
  packages. Private mirrors can use explicitly configured Go authentication and
89
89
  checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
90
90
 
91
- CLI 0.41.5 uses templates ~0.35.3, whose default SDK pin is 0.38.2. Compose import and SDK defaults
91
+ CLI 0.41.7 uses templates ~0.35.5, whose default SDK pin is 0.38.2. Compose import and SDK defaults
92
92
  read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
93
93
  Every App method must be declared in the permissions table. Dynamic names and
94
94
  custom HTTP dispatchers are rejected. With SDK 0.33.0, use fixed
@@ -99,7 +99,7 @@ unimported development commands are excluded. Use
99
99
  authentication. See [App entry and registration](docs/app-entry.md).
100
100
 
101
101
  `init` and `add` read their templates from the locally installed
102
- `@synopackageland/templates`. CLI 0.41.5 depends on `~0.35.3`: npm installation or
102
+ `@synopackageland/templates`. CLI 0.41.7 depends on `~0.35.5`: npm installation or
103
103
  update can select the latest templates patch in the 0.35 minor line. Templates
104
104
  patches need only a templates release; update the npm installation to receive them.
105
105
  The CLI never fetches templates at runtime, so offline use works after installation;
@@ -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 with
279
- Application Privileges (N), and a fresh anonymous browser context (X).
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
@@ -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);
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
- /** 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
@@ -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 with app permission.`;
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
- /** Registers A (admin), N (permitted normal user), X (fresh anonymous context). */
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
- test(`${entry.package}: app opens — ${role}`, {
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 marker(root, options.ready).waitFor({ state: 'visible', timeout });
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
- 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,
@@ -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.6",
3
+ "version": "0.41.8",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
package/skills/SKILL.md CHANGED
@@ -26,7 +26,7 @@ Use this guide to initialize, build and inspect own applications or upstream OSS
26
26
 
27
27
  For an existing open-source App, first read [reference/upstream-fork.md](reference/upstream-fork.md): upstream fork/history + untouched upstream files + all packaging in `synology/`, official container images pinned by tag/index digest, native official release binaries pinned by SHA-256 or a source build hook, and a single tag-merge upgrade command. Use `init --upstream-fork`; never rewrite the App as a component. Integrated upstream Apps require direct DSM administrator entry without a separate App password or an OIDC/SSO server: supported trusted headers first, disabling native login second; the shipped OpenList pattern uses the existing SDK 0.17.3 token adapter (`mode: token`, `access: dsm-admins`, `entry: admin-only`) with server-side Authorization injection. All DSM admins share one upstream admin; token reset requires package restart; WebDAV retains its native credentials.
28
28
 
29
- Use SDK/App 0.38.2 and CLI 0.41.5 for App entry enforcement. Declare every App method
29
+ Use SDK/App 0.38.3 and CLI 0.41.7 for App entry enforcement. Declare every App method
30
30
  in the permissions table; dynamic names and unregistered HTTP dispatchers are rejected.
31
31
  Use fixed SDK routes and `identity_gateway.upstream_auth_paths` for client API
32
32
  paths that rely on upstream authentication. Read
@@ -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.
@@ -1,6 +1,6 @@
1
1
  # Native installation settings
2
2
 
3
- Use SDK/App 0.38.2, CLI 0.41.5 and templates 0.35.3. Configure installation through the manifest and generated wizard; do not add App ACL writers, root hooks or independent port state.
3
+ Use SDK/App 0.38.3, CLI 0.41.7 and templates 0.35.5. Configure installation through the manifest and generated wizard; do not add App ACL writers, root hooks or independent port state.
4
4
 
5
5
  ```yaml
6
6
  install: