@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 +19 -0
- package/README.md +12 -2
- package/dist/e2e/app-open.d.ts +3 -0
- package/dist/e2e/app-open.js +2 -1
- package/dist/e2e/certificates.d.ts +6 -0
- package/dist/e2e/certificates.js +60 -0
- package/dist/e2e/dsm.d.ts +4 -2
- package/dist/e2e/dsm.js +28 -8
- package/dist/standalone/compose.d.ts +15 -0
- package/dist/standalone/compose.js +14 -5
- package/dist/standalone/inspect-spk.js +7 -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,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
|
|
279
|
-
|
|
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
|
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);
|
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|