@synopackageland/cli 0.41.5 → 0.41.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 +13 -0
- package/README.md +28 -4
- package/dist/e2e/app-open.d.ts +27 -0
- package/dist/e2e/app-open.js +43 -0
- package/dist/e2e/dsm.d.ts +8 -1
- package/dist/e2e/dsm.js +126 -1
- package/dist/init.js +1 -0
- package/dist/runtime/webapi-profile.js +1 -1
- package/package.json +2 -2
- package/skills/SKILL.md +15 -2
- package/skills/reference/install.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.41.7 (2026-10-30)
|
|
4
|
+
|
|
5
|
+
- Allow `webapi` raw profiles to set `raw_max_bytes` up to 10 GiB
|
|
6
|
+
(10737418240 bytes), matching SDK/App 0.38.3 and executor/v0.38.3. The limit
|
|
7
|
+
shipped in 0.41.6 without a changelog entry. Larger raw downloads need SDK
|
|
8
|
+
0.38.3; templates ~0.35.5 still pin SDK 0.38.2. Raising a profile limit
|
|
9
|
+
requires administrator approval and fresh user consent.
|
|
10
|
+
|
|
11
|
+
## 0.41.6
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
- Scaffold the smoke test, runner config and npm script from templates; document the existing credential/profile/tunnel path.
|
|
15
|
+
|
|
3
16
|
## 0.41.5
|
|
4
17
|
|
|
5
18
|
- Generate dedicated shared folders and guarded existing-folder choices, retain upgrade selections, validate required HTTPS settings before Next, and enable cold sign-in resume for all gateway modes. Pin templates 0.35.3 and SDK 0.38.2.
|
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.
|
|
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.
|
|
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;
|
|
@@ -223,8 +223,7 @@ not change how the App runs or its privilege tier. See `synopkgland skill show b
|
|
|
223
223
|
|
|
224
224
|
`@synopackageland/cli/e2e` holds the DSM helpers for a package's real-NAS
|
|
225
225
|
suite on [`e2e`](https://www.npmjs.com/package/e2e) and `@e2e-dev/web`
|
|
226
|
-
(optional peers, with `ai` and `@ai-sdk/openai`).
|
|
227
|
-
`src/e2e/dsm.ts`, can be copied verbatim to `tests/e2e/support/dsm.ts`.
|
|
226
|
+
(optional peers, with `ai` and `@ai-sdk/openai`).
|
|
228
227
|
|
|
229
228
|
```ts
|
|
230
229
|
// e2e.config.ts
|
|
@@ -263,6 +262,31 @@ requests with `context.route` and `route.fetch` should end with
|
|
|
263
262
|
`await context.unrouteAll({behavior: 'ignoreErrors'})`: a request still in
|
|
264
263
|
flight otherwise rejects into a run error that carries its headers.
|
|
265
264
|
|
|
265
|
+
### App-open smoke
|
|
266
|
+
|
|
267
|
+
Every template includes `npm run e2e:smoke` and a small test. Run `npm install`
|
|
268
|
+
at the project root and `npx @e2e-dev/web install chromium` once before using it:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { appOpenSmoke } from '@synopackageland/cli/e2e';
|
|
272
|
+
appOpenSmoke({ ready: '[data-app-ready]' });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`ready` is an app-content CSS selector or `{text: 'Exact text'}`. The helper
|
|
276
|
+
reads `dsm-package.yaml` (`app` overrides its filename) and derives the launcher
|
|
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).
|
|
280
|
+
N explicitly skips when neither `DSM_E2E_ACCOUNT_NORMAL` nor its saved profile
|
|
281
|
+
exists; `normal` selects another named account/profile. A defaults to `admin`.
|
|
282
|
+
An expired saved profile needs its account file to sign in again and fails if
|
|
283
|
+
unavailable. Invalid configured account files fail instead of being skipped.
|
|
284
|
+
Use the same `DSM_E2E_*` account, target and SSH settings described above.
|
|
285
|
+
The anonymous test requires a DSM gate and refuses ready app content or error
|
|
286
|
+
pages. Only entries the manifest declares upstream-authenticated may provide
|
|
287
|
+
`publicLogin` (a selector or text marker for their own login page).
|
|
288
|
+
The smoke never installs packages or changes accounts/privileges.
|
|
289
|
+
|
|
266
290
|
## AI assistants
|
|
267
291
|
|
|
268
292
|
Every project includes `AGENTS.md` and a `CLAUDE.md` pointer. Start with
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SmokeMarker = string | {
|
|
2
|
+
selector: string;
|
|
3
|
+
} | {
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
export interface AppOpenSmokeOptions {
|
|
7
|
+
/** Manifest filename; defaults to dsm-package.yaml in the project root. */
|
|
8
|
+
app?: string;
|
|
9
|
+
/** Override the manifest-derived entry, e.g. a configured Portal alias. */
|
|
10
|
+
entry?: string;
|
|
11
|
+
ready: SmokeMarker;
|
|
12
|
+
/** Required only for an entry the manifest declares upstream-authenticated. */
|
|
13
|
+
publicLogin?: SmokeMarker;
|
|
14
|
+
admin?: string;
|
|
15
|
+
normal?: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface SmokeEntry {
|
|
19
|
+
package: string;
|
|
20
|
+
appId: string;
|
|
21
|
+
entry: string;
|
|
22
|
+
window: boolean;
|
|
23
|
+
upstreamAuthenticated: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Uses the same launch defaults and legacy web entry resolver as packaging. */
|
|
26
|
+
export declare function deriveSmokeEntry(raw: any, entry?: string): SmokeEntry;
|
|
27
|
+
export declare function readSmokeEntry(options: Pick<AppOpenSmokeOptions, 'app' | 'entry'>): SmokeEntry;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { parse } from 'yaml';
|
|
3
|
+
import { resolveAppLaunch } from '../contract/app-launch.js';
|
|
4
|
+
import { trustedAppEntryPath } from '../contract/web-launch.js';
|
|
5
|
+
function safeEntry(entry) {
|
|
6
|
+
const url = new URL(entry, 'https://dsm.example');
|
|
7
|
+
if ((!entry.startsWith('/') && !/^https?:\/\//.test(entry)) || entry.startsWith('//') ||
|
|
8
|
+
/[\\\r\n]/.test(entry) || !['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
|
|
9
|
+
throw new Error('Smoke entry must be a same-origin path or an HTTP(S) URL without credentials.');
|
|
10
|
+
}
|
|
11
|
+
return entry;
|
|
12
|
+
}
|
|
13
|
+
/** Uses the same launch defaults and legacy web entry resolver as packaging. */
|
|
14
|
+
export function deriveSmokeEntry(raw, entry) {
|
|
15
|
+
if (!raw || typeof raw.package !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(raw.package)) {
|
|
16
|
+
throw new Error('Smoke requires a package id in dsm-package.yaml.');
|
|
17
|
+
}
|
|
18
|
+
const native = raw.schema === 2;
|
|
19
|
+
const gateway = raw.identity_gateway;
|
|
20
|
+
const web = Boolean(gateway || raw.compose || (!native && raw.adminport));
|
|
21
|
+
if ((!native && !raw.adminport) || (raw.ui === false && !gateway))
|
|
22
|
+
throw new Error('This package has no app entry to smoke test.');
|
|
23
|
+
const appId = native ? `SYNO.SDS.${raw.package}.Instance` : raw.package;
|
|
24
|
+
const launch = resolveAppLaunch(native ? raw.ui?.launch : raw.integration?.web?.launch, web);
|
|
25
|
+
if (!launch)
|
|
26
|
+
throw new Error('Invalid app launch mode in manifest.');
|
|
27
|
+
const window = launch === 'dsm-window' && !raw.console && raw.ui !== false && !raw.compose && !raw.daemon?.ingress;
|
|
28
|
+
const derived = raw.daemon?.ingress?.path ?? (gateway
|
|
29
|
+
? `/webman/3rdparty/${raw.package}/sso/open.cgi`
|
|
30
|
+
: native ? `/?launchApp=${encodeURIComponent(appId)}`
|
|
31
|
+
: trustedAppEntryPath(raw));
|
|
32
|
+
const selected = safeEntry(entry ?? derived);
|
|
33
|
+
const pathname = new URL(selected, 'https://dsm.example').pathname;
|
|
34
|
+
// The menu CGI always checks DSM, even when the upstream listener is public.
|
|
35
|
+
const menuEntry = `/webman/3rdparty/${raw.package}/sso/open.cgi`;
|
|
36
|
+
const upstreamAuthenticated = (gateway?.upstream_access === 'public' && pathname !== menuEntry) ||
|
|
37
|
+
(!native && raw.integration?.web?.access === 'public') ||
|
|
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 };
|
|
40
|
+
}
|
|
41
|
+
export function readSmokeEntry(options) {
|
|
42
|
+
return deriveSmokeEntry(parse(readFileSync(options.app ?? 'dsm-package.yaml', 'utf8')), options.entry);
|
|
43
|
+
}
|
package/dist/e2e/dsm.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type AppOpenSmokeOptions, type SmokeMarker } from "./app-open.js";
|
|
2
|
+
export { deriveSmokeEntry, type AppOpenSmokeOptions, type SmokeMarker } from "./app-open.js";
|
|
1
3
|
import { surfaceOf } from "@e2e-dev/web";
|
|
2
4
|
type Env = Readonly<Record<string, string | undefined>>;
|
|
3
5
|
type App = {
|
|
@@ -187,4 +189,9 @@ export declare function waitForPortal(page?: Page): Promise<void>;
|
|
|
187
189
|
* tab. With the `browser` fixture, native dialogs are accepted and recorded (see handleDialogs).
|
|
188
190
|
*/
|
|
189
191
|
export declare function openPortal(app: App, path: string, browser?: DialogBrowser): Promise<Page>;
|
|
190
|
-
|
|
192
|
+
/** Missing normal accounts are skipped; malformed configured files still fail. */
|
|
193
|
+
export declare function normalSmokeSkip(user?: string, env?: Env): string | undefined;
|
|
194
|
+
/** Checks rendered errors as well as HTTP errors; ready alone never proves success. */
|
|
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). */
|
|
197
|
+
export declare function appOpenSmoke(options: AppOpenSmokeOptions): void;
|
package/dist/e2e/dsm.js
CHANGED
|
@@ -14,12 +14,14 @@ import { sessionFetch } from "../nas/transport.js";
|
|
|
14
14
|
import { canonicalOrigin } from "../nas/transport.js";
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
|
-
import { chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { connect, createServer } from "node:net";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, join, resolve } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
23
|
+
import { readSmokeEntry } from "./app-open.js";
|
|
24
|
+
export { deriveSmokeEntry } from "./app-open.js";
|
|
23
25
|
import { surfaceOf, test, web } from "@e2e-dev/web";
|
|
24
26
|
const safeName = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
25
27
|
const desktopSelector = "#sds-desktop";
|
|
@@ -576,6 +578,129 @@ export async function openPortal(app, path, browser) {
|
|
|
576
578
|
await waitForPortal(page);
|
|
577
579
|
return page;
|
|
578
580
|
}
|
|
581
|
+
// ---- Minimal app-open smoke -----------------------------------------------
|
|
582
|
+
/** Missing normal accounts are skipped; malformed configured files still fail. */
|
|
583
|
+
export function normalSmokeSkip(user = "normal", env = process.env) {
|
|
584
|
+
const key = `DSM_E2E_ACCOUNT_${user.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
585
|
+
if (env[key])
|
|
586
|
+
return undefined;
|
|
587
|
+
const file = profilePath(env.DSM_E2E_TARGET || "dsm", user, env);
|
|
588
|
+
if (existsSync(file) && loadProfile(file, dsmUrl(env)))
|
|
589
|
+
return undefined;
|
|
590
|
+
return `Normal-user smoke requires ${key} (mode 600) or a saved ${user} profile with app permission.`;
|
|
591
|
+
}
|
|
592
|
+
function marker(root, value) {
|
|
593
|
+
return typeof value === "string" ? root.locator(value).first()
|
|
594
|
+
: "selector" in value ? root.locator(value.selector).first() : root.getByText(value.text, { exact: true }).first();
|
|
595
|
+
}
|
|
596
|
+
const dsmGate = loginSelector;
|
|
597
|
+
async function dsmLoginVisible(page) {
|
|
598
|
+
if (await page.locator('[data-error-code="dsm_sign_in_required"], [data-i18n="gateway.sign_in"]').first().isVisible())
|
|
599
|
+
return true;
|
|
600
|
+
if (await page.getByText(signInText).first().isVisible() &&
|
|
601
|
+
await page.locator('a, button, form, input').first().isVisible())
|
|
602
|
+
return true;
|
|
603
|
+
if (!await page.locator(dsmGate).first().isVisible())
|
|
604
|
+
return false;
|
|
605
|
+
const url = new URL(page.url());
|
|
606
|
+
// An upstream username field is not proof of a DSM gate. Alias pages also
|
|
607
|
+
// carry DSM's login root; ordinary upstream forms cannot stand in for it.
|
|
608
|
+
return (url.origin === new URL(dsmUrl()).origin && (url.pathname === '/' || url.pathname.startsWith('/webman/'))) ||
|
|
609
|
+
await page.locator('#sds-login, #sds-login-vue').first().isVisible();
|
|
610
|
+
}
|
|
611
|
+
const signInText = /sign in to DSM|log in to DSM|\u767b\u5165 DSM|\u767b\u5f55 DSM/i;
|
|
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
|
+
/** Checks rendered errors as well as HTTP errors; ready alone never proves success. */
|
|
614
|
+
export async function assertSmokePage(page, ready, anonymous = false, publicLogin) {
|
|
615
|
+
if (await page.locator('body').innerText().then(text => errorText.test(text)))
|
|
616
|
+
throw new Error('App smoke reached an error page.');
|
|
617
|
+
if (anonymous) {
|
|
618
|
+
if (await marker(page, ready).isVisible())
|
|
619
|
+
throw new Error('Anonymous app smoke exposed app content.');
|
|
620
|
+
if (publicLogin) {
|
|
621
|
+
if (!await marker(page, publicLogin).isVisible())
|
|
622
|
+
throw new Error('Declared upstream login is not visible.');
|
|
623
|
+
}
|
|
624
|
+
else if (!await dsmLoginVisible(page)) {
|
|
625
|
+
throw new Error('Anonymous app smoke did not reach the DSM login gate.');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else if (await dsmLoginVisible(page) || await page.getByText(signInText).first().isVisible()) {
|
|
629
|
+
throw new Error('Signed-in app smoke reached a login gate.');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/** Registers A (admin), N (permitted normal user), X (fresh anonymous context). */
|
|
633
|
+
export function appOpenSmoke(options) {
|
|
634
|
+
const entry = readSmokeEntry(options);
|
|
635
|
+
if (entry.upstreamAuthenticated && !options.publicLogin) {
|
|
636
|
+
throw new Error('An upstream-authenticated smoke entry requires publicLogin.');
|
|
637
|
+
}
|
|
638
|
+
if (!entry.upstreamAuthenticated && options.publicLogin) {
|
|
639
|
+
throw new Error('publicLogin requires a manifest-declared upstream-authenticated entry.');
|
|
640
|
+
}
|
|
641
|
+
const timeout = options.timeout ?? 90_000;
|
|
642
|
+
const normal = options.normal ?? 'normal';
|
|
643
|
+
const admin = options.admin ?? 'admin';
|
|
644
|
+
if (normal === admin)
|
|
645
|
+
throw new Error('Normal smoke must use a separate named profile.');
|
|
646
|
+
for (const [role, user] of [['A administrator', admin], ['N normal user', normal]]) {
|
|
647
|
+
test(`${entry.package}: app opens — ${role}`, {
|
|
648
|
+
timeout, skip: user === normal ? normalSmokeSkip(normal) : undefined,
|
|
649
|
+
}, async ({ app, browser }) => {
|
|
650
|
+
await signIn(app, user);
|
|
651
|
+
const page = dsmPage();
|
|
652
|
+
const isAdmin = await page.evaluate(() => window.SYNO?.SDS?.Session?.is_admin);
|
|
653
|
+
if (isAdmin !== (user === admin))
|
|
654
|
+
throw new Error('Smoke account does not have the expected DSM administrator role.');
|
|
655
|
+
const failures = [];
|
|
656
|
+
const response = (r) => { if (r.status() >= 500)
|
|
657
|
+
failures.push(r.status()); };
|
|
658
|
+
page.on('response', response);
|
|
659
|
+
try {
|
|
660
|
+
const root = entry.window ? await openApp(app, entry.appId, timeout, browser)
|
|
661
|
+
: (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);
|
|
664
|
+
if (failures.length)
|
|
665
|
+
throw new Error(`App smoke received HTTP ${failures.join(', ')}.`);
|
|
666
|
+
}
|
|
667
|
+
finally {
|
|
668
|
+
page.off('response', response);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
test(`${entry.package}: app opens — X signed out`, { timeout }, async ({ app }) => {
|
|
673
|
+
// The runner owns an attempt context; a separate one cannot inherit sessions,
|
|
674
|
+
// storage, routes or service workers from setup tests or global hooks.
|
|
675
|
+
await openPath(app, '/');
|
|
676
|
+
const browser = surface().context().browser();
|
|
677
|
+
if (!browser)
|
|
678
|
+
throw new Error('Anonymous smoke requires a browser that supports a fresh context.');
|
|
679
|
+
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
|
680
|
+
try {
|
|
681
|
+
const page = await context.newPage();
|
|
682
|
+
const failures = [];
|
|
683
|
+
page.on('response', r => { if (r.status() >= 500)
|
|
684
|
+
failures.push(r.status()); });
|
|
685
|
+
const response = await page.goto(new URL(entry.entry, dsmUrl()).href);
|
|
686
|
+
if (response && response.status() >= 400 &&
|
|
687
|
+
(response.status() !== 401 || !response.headers()['content-type']?.includes('text/html'))) {
|
|
688
|
+
throw new Error(`Anonymous app entry returned HTTP ${response.status()}.`);
|
|
689
|
+
}
|
|
690
|
+
const login = options.publicLogin && marker(page, options.publicLogin);
|
|
691
|
+
if (login)
|
|
692
|
+
await login.waitFor({ state: 'visible', timeout });
|
|
693
|
+
else
|
|
694
|
+
await page.locator(`${dsmGate}, [data-error-code="dsm_sign_in_required"], [data-i18n="gateway.sign_in"]`).or(page.getByText(signInText)).first().waitFor({ state: 'visible', timeout });
|
|
695
|
+
await assertSmokePage(page, options.ready, true, options.publicLogin);
|
|
696
|
+
if (failures.length)
|
|
697
|
+
throw new Error(`Anonymous app smoke received HTTP ${failures.join(', ')}.`);
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
await context.close();
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
}
|
|
579
704
|
// ---- Command line ---------------------------------------------------------
|
|
580
705
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
581
706
|
const [verb, separator, ...command] = process.argv.slice(2);
|
package/dist/init.js
CHANGED
|
@@ -207,6 +207,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
207
207
|
"AGENTS.md",
|
|
208
208
|
"CLAUDE.md",
|
|
209
209
|
...sharedScripts,
|
|
210
|
+
...["package.json", "e2e.config.ts", "tests/e2e/smoke.e2e.ts"].filter(file => existsSync(join(shared, file))),
|
|
210
211
|
];
|
|
211
212
|
const files = [
|
|
212
213
|
...templateFiles(source),
|
|
@@ -8,7 +8,7 @@ export function validWebAPIProfiles(value) {
|
|
|
8
8
|
for (const [name, p] of Object.entries(value)) {
|
|
9
9
|
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(name) || name === 'home' || !record(p) || Object.keys(p).some(k => !['purpose', 'apis', 'raw_max_bytes'].includes(k)) || typeof p.purpose !== 'string' || !p.purpose.trim() || Buffer.byteLength(p.purpose) > 1024 || /[\x00-\x1f\x7f]/.test(p.purpose) || !record(p.apis) || !Object.keys(p.apis).length)
|
|
10
10
|
return false;
|
|
11
|
-
if (p.raw_max_bytes !== undefined && (!Number.isSafeInteger(p.raw_max_bytes) || Number(p.raw_max_bytes) < 0 || Number(p.raw_max_bytes) >
|
|
11
|
+
if (p.raw_max_bytes !== undefined && (!Number.isSafeInteger(p.raw_max_bytes) || Number(p.raw_max_bytes) < 0 || Number(p.raw_max_bytes) > 10737418240))
|
|
12
12
|
return false;
|
|
13
13
|
for (const [api, methods] of Object.entries(p.apis)) {
|
|
14
14
|
if (!/^SYNO\.[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)*$/.test(api) || api.toLowerCase().split('.').slice(1).some(x => blocked.has(x)) || !record(methods) || !Object.keys(methods).length)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.7",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@napi-rs/keyring": "^2.1.1",
|
|
31
|
-
"@synopackageland/templates": "~0.35.
|
|
31
|
+
"@synopackageland/templates": "~0.35.5",
|
|
32
32
|
"ajv": "^8.17.1",
|
|
33
33
|
"commander": "^13.1.0",
|
|
34
34
|
"json-canonicalize": "^3.0.1",
|
package/skills/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
|
|
|
5
5
|
|
|
6
6
|
# Independent Synology SPKs
|
|
7
7
|
|
|
8
|
-
The CLI installs templates through `~0.35.
|
|
8
|
+
The CLI installs templates through `~0.35.5`. A templates patch needs only a
|
|
9
9
|
templates release; npm install/update selects a patch in that minor line. The CLI
|
|
10
10
|
uses the locally installed templates and reads their SDK pin for defaults; it
|
|
11
11
|
never downloads templates at runtime. Preserve the lockfile and use `npm ci` for
|
|
@@ -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.
|
|
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
|
|
@@ -307,3 +307,16 @@ Headless Compose 不產生 launcher。health 必須選實際 endpoint 與 2xx st
|
|
|
307
307
|
readiness 不跟隨 redirect,check 不會發 HTTP。Compose 不支援 ctl。
|
|
308
308
|
|
|
309
309
|
For controlling installed Apps or designing task-level AI commands, read [reference/ctl.md](reference/ctl.md). Use ctl host list, register/renew explicitly, discover all commands, and always name the host when agents may operate multiple NAS machines.
|
|
310
|
+
|
|
311
|
+
## App-open smoke
|
|
312
|
+
|
|
313
|
+
Every generated project has `tests/e2e/smoke.e2e.ts` and `npm run e2e:smoke`.
|
|
314
|
+
Use `appOpenSmoke` from `@synopackageland/cli/e2e`; keep one content-specific
|
|
315
|
+
`ready` selector or text marker. It derives the manifest entry and launcher,
|
|
316
|
+
reuses the existing DSM credentials/profiles/tunnel, and registers administrator,
|
|
317
|
+
normal-user and fresh anonymous tests. Configure `DSM_E2E_ACCOUNT_ADMIN` and
|
|
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.
|
|
320
|
+
Override `app` for a manifest filename or `entry` for a configured URL.
|
|
321
|
+
Only a manifest-declared upstream-authenticated entry may use `publicLogin`.
|
|
322
|
+
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.
|
|
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:
|