@synopackageland/cli 0.9.0 → 0.9.1
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 +144 -1
- package/README.md +1 -1
- package/dist/add.js +12 -0
- package/dist/build-spk/dsm-version.d.ts +0 -2
- package/dist/build-spk/dsm-version.js +0 -6
- package/dist/build-spk/generator.js +4 -23
- package/dist/build-spk/resource-worker.js +0 -3
- package/dist/build-spk/uninstall-wizard.d.ts +8 -3
- package/dist/build-spk/uninstall-wizard.js +4 -2
- package/dist/build-spk/wizard-messages.d.ts +33 -0
- package/dist/build-spk/wizard-messages.js +33 -0
- package/dist/cli.js +3 -3
- package/dist/contract/validate.js +4 -21
- package/dist/error-registry.js +11 -12
- package/dist/host/dsm-client.js +51 -30
- package/dist/host/types.d.ts +5 -0
- package/dist/info.js +14 -1
- package/dist/init.js +1 -1
- package/dist/portal/alias.d.ts +6 -4
- package/dist/portal/alias.js +6 -11
- package/dist/portal/package.d.ts +0 -3
- package/dist/portal/package.js +1 -7
- package/dist/runtime/executor-digests.json +11 -0
- package/dist/runtime/uninstall-cleanup.d.ts +7 -1
- package/dist/runtime/uninstall-cleanup.js +7 -1
- package/dist/standalone/dsm-window.js +8 -11
- package/dist/standalone/inspect-spk.js +8 -7
- package/dist/standalone/manifest.js +12 -3
- package/dist/standalone/root-window.d.ts +10 -3
- package/dist/standalone/root-window.js +54 -29
- package/dist/standalone/spk.js +1 -1
- package/dist/validate.js +2 -1
- package/package.json +3 -3
- package/schema/synology-app.schema.json +1 -10
- package/skills/SKILL.md +1 -1
- package/skills/reference/create.md +7 -2
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/identity.md +1 -1
- package/skills/reference/portal.md +7 -3
package/dist/host/dsm-client.js
CHANGED
|
@@ -92,6 +92,43 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
92
92
|
if (!cookieHeader)
|
|
93
93
|
throw new Error("DSM login returned no session cookie");
|
|
94
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* One read-only call of the App's own CGI as the administrator. identity:
|
|
97
|
+
* standalone wraps business methods in an App session: trade the DSM cookie
|
|
98
|
+
* for one (same Origin), read once, then end that session. Undefined when
|
|
99
|
+
* the App refuses; `error` when no App session could be opened.
|
|
100
|
+
*/
|
|
101
|
+
const readApp = async (packageIdentity, method) => {
|
|
102
|
+
await ensureLogin();
|
|
103
|
+
const appCall = async (name, cookie) => {
|
|
104
|
+
const response = await dsmFetch(`${dsmOrigin}/webman/3rdparty/${encodeURIComponent(packageIdentity)}/cgi/api.cgi`, {
|
|
105
|
+
method: "POST", headers: { cookie, origin: dsmOrigin, "content-type": "application/json" },
|
|
106
|
+
body: JSON.stringify({ method: name, params: name === "identity.session" ? { bootstrap: true } : {} }),
|
|
107
|
+
});
|
|
108
|
+
const body = await response.json();
|
|
109
|
+
return { body, setCookie: setCookiePairs(response.headers) };
|
|
110
|
+
};
|
|
111
|
+
const direct = await appCall(method, cookieHeader ?? "");
|
|
112
|
+
if (direct.body.success)
|
|
113
|
+
return { data: direct.body.data };
|
|
114
|
+
if (direct.body.error?.code !== "session_required")
|
|
115
|
+
return undefined;
|
|
116
|
+
const session = await appCall("identity.session", cookieHeader ?? "");
|
|
117
|
+
if (!session.body.success) {
|
|
118
|
+
const code = session.body.error?.code;
|
|
119
|
+
return { error: code === "https_required"
|
|
120
|
+
? "identity: standalone status needs an https:// DSM host profile to open an App session."
|
|
121
|
+
: `identity: standalone App session bootstrap failed (${code ?? "error"}).` };
|
|
122
|
+
}
|
|
123
|
+
const appCookie = [cookieHeader ?? "", ...session.setCookie].filter(Boolean).join("; ");
|
|
124
|
+
try {
|
|
125
|
+
const status = await appCall(method, appCookie);
|
|
126
|
+
return status.body.success ? { data: status.body.data } : undefined;
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await appCall("identity.logout", appCookie).catch(() => undefined);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
95
132
|
return {
|
|
96
133
|
dsmOrigin,
|
|
97
134
|
async installFromLocalSpk(input) {
|
|
@@ -108,39 +145,23 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
108
145
|
},
|
|
109
146
|
async getExecutorStatus(packageIdentity) {
|
|
110
147
|
try {
|
|
111
|
-
await ensureLogin();
|
|
112
|
-
const appCall = async (method, cookie) => {
|
|
113
|
-
const response = await dsmFetch(`${dsmOrigin}/webman/3rdparty/${encodeURIComponent(packageIdentity)}/cgi/api.cgi`, {
|
|
114
|
-
method: "POST", headers: { cookie, origin: dsmOrigin, "content-type": "application/json" },
|
|
115
|
-
body: JSON.stringify({ method, params: method === "identity.session" ? { bootstrap: true } : {} }),
|
|
116
|
-
});
|
|
117
|
-
const body = await response.json();
|
|
118
|
-
return { body, setCookie: setCookiePairs(response.headers) };
|
|
119
|
-
};
|
|
120
148
|
// A paused executor answers with why and what to do next.
|
|
121
149
|
const explain = (status) => status.state === "paused" ? { ...status, error: `Executor paused (${status.reason}). ${status.next}` } : status;
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
return explain(direct.body.data);
|
|
125
|
-
// identity: standalone wraps business methods in an App session; trade the
|
|
126
|
-
// administrator's DSM cookie for one, read once, then end that session.
|
|
127
|
-
if (direct.body.error?.code !== "session_required")
|
|
150
|
+
const read = await readApp(packageIdentity, "executor.status");
|
|
151
|
+
if (!read)
|
|
128
152
|
return undefined;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
finally {
|
|
142
|
-
await appCall("identity.logout", appCookie).catch(() => undefined);
|
|
143
|
-
}
|
|
153
|
+
if ("error" in read)
|
|
154
|
+
return { ready: false, executor_digest: "", policy_digest: "", error: read.error };
|
|
155
|
+
return explain(read.data);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
async getPortalStatus(packageIdentity) {
|
|
162
|
+
try {
|
|
163
|
+
const read = await readApp(packageIdentity, "portal.status");
|
|
164
|
+
return read && "data" in read ? read.data : undefined;
|
|
144
165
|
}
|
|
145
166
|
catch {
|
|
146
167
|
return undefined;
|
package/dist/host/types.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface NasHost {
|
|
|
35
35
|
policy_digest: string;
|
|
36
36
|
error?: string;
|
|
37
37
|
} | undefined>;
|
|
38
|
+
/** The App's `portal.status`: whether an administrator set its Application Portal alias. */
|
|
39
|
+
getPortalStatus?(packageIdentity: string): Promise<{
|
|
40
|
+
state: string;
|
|
41
|
+
alias?: string;
|
|
42
|
+
} | undefined>;
|
|
38
43
|
getArchFamily?(): Promise<NativeArchFamily | undefined>;
|
|
39
44
|
/** List the current logical Shared Folder names using an administrator session. */
|
|
40
45
|
listShareNames?(): Promise<string[]>;
|
package/dist/info.js
CHANGED
|
@@ -161,13 +161,26 @@ export async function standaloneInfo(options) {
|
|
|
161
161
|
reason: "Installed package supports this capability without administrator approval.",
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
|
+
if (m.portal)
|
|
165
|
+
capabilities.push(installed ? await portalCapability(m.package, m.portal.alias, options.host) : {
|
|
166
|
+
name: "portal", state: "blocked", reason: "Package is not installed; deploy first.",
|
|
167
|
+
});
|
|
164
168
|
return {
|
|
165
169
|
...state,
|
|
166
170
|
capabilities,
|
|
167
171
|
...(executor ? { executor } : {}),
|
|
168
|
-
uninstallCleanup: uninstallCleanup(m.package, Boolean(m.capabilities.rootOps || m.capabilities.delegate)),
|
|
172
|
+
uninstallCleanup: uninstallCleanup(m.package, { executor: Boolean(m.capabilities.rootOps || m.capabilities.delegate), portal: Boolean(m.portal) }),
|
|
169
173
|
};
|
|
170
174
|
}
|
|
175
|
+
/** What the installed App's portal.status says; DSM owns the alias, an administrator sets it. */
|
|
176
|
+
async function portalCapability(pkg, defaultAlias, host) {
|
|
177
|
+
const status = await host.getPortalStatus?.(pkg);
|
|
178
|
+
if (status?.state === "active" && status.alias)
|
|
179
|
+
return { name: "portal", state: "ready", reason: `Users open the App at ${host.dsmOrigin.replace(/\/$/, "")}/${encodeURIComponent(status.alias)}/ after DSM sign-in.` };
|
|
180
|
+
if (status?.state === "inactive")
|
|
181
|
+
return { name: "portal", state: "needs-admin-approval", reason: `No alias is set; an administrator applies one (default ${defaultAlias}) in the App's Application Portal panel.` };
|
|
182
|
+
return { name: "portal", state: "blocked", reason: "The App's portal.status could not be read; open the App window to check the Application Portal." };
|
|
183
|
+
}
|
|
171
184
|
/** A native layout has no SDK capabilities; report package state only. */
|
|
172
185
|
async function nativeLayoutInfo(root, host) {
|
|
173
186
|
const loaded = loadNativeLayout(root);
|
package/dist/init.js
CHANGED
|
@@ -119,7 +119,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
119
119
|
const shared = join(templatesRoot(), "_shared");
|
|
120
120
|
const sharedFiles = ["LICENSES/TEMPLATE.txt", "AGENTS.md", "CLAUDE.md"];
|
|
121
121
|
const files = [...templateFiles(source), ...sharedFiles, ...overlays.flatMap(root => templateFiles(root)),
|
|
122
|
-
...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : [])];
|
|
122
|
+
...capability.borrowed.map(borrowedTarget), ...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : [])];
|
|
123
123
|
const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
|
|
124
124
|
if (collisions.length > 0) {
|
|
125
125
|
throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} 已有 ${collisions.join(", ")};init 不會覆蓋既有檔案。請改用空目錄。`);
|
package/dist/portal/alias.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AppContract } from "../types.js";
|
|
2
1
|
/**
|
|
3
2
|
* DSM Application Portal alias: `https://<nas>/<alias>/` opens the App after
|
|
4
3
|
* DSM sign-in. DSM itself only refuses empty aliases (4100), synoman
|
|
@@ -12,6 +11,12 @@ export declare const PORTAL_ALIAS_MAX = 64;
|
|
|
12
11
|
* of Synology Apps, as observed on DSM 7.4-90069. DSM stays the authority.
|
|
13
12
|
*/
|
|
14
13
|
export declare const RESERVED_PORTAL_ALIASES: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* A portal needs the executor tier: DSM removes the alias files on uninstall
|
|
16
|
+
* but leaves nginx serving the alias, and only the root executor purge can
|
|
17
|
+
* reload it. A no-privilege App never runs anything as root.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PORTAL_REQUIRES_EXECUTOR = "portal \u9700\u8981 executor \u5C64\u7D1A\uFF08capabilities.rootOps \u6216 delegate\uFF09\uFF1A\u89E3\u9664\u5B89\u88DD\u5F8C DSM \u4E0D\u6703 reload nginx\uFF0C\u767B\u5165\u5165\u53E3\u5225\u540D\u8981\u7531 root executor \u6E05\u9664\u3002\u6C92\u6709 executor \u7684 App \u8ACB\u79FB\u9664 portal\uFF0C\u6539\u7531\u7BA1\u7406\u54E1\u5728 DSM \u624B\u52D5\u8A2D\u5B9A\u53CD\u5411\u4EE3\u7406\u3002";
|
|
15
20
|
export interface PortalDeclaration {
|
|
16
21
|
alias: string;
|
|
17
22
|
}
|
|
@@ -20,6 +25,3 @@ export declare function isPortalAlias(value: unknown): value is string;
|
|
|
20
25
|
export declare function defaultPortalAlias(packageId: string): string;
|
|
21
26
|
/** `{}` or `{alias}`; undefined for any other shape. */
|
|
22
27
|
export declare function readPortalDeclaration(value: unknown, packageId: string): PortalDeclaration | undefined;
|
|
23
|
-
/** A Compose App reaches the portal only as a DSM window behind the DSM-authenticated gateway. */
|
|
24
|
-
export declare function contractPortalSupported(contract: AppContract): boolean;
|
|
25
|
-
export declare function resolvedContractPortal(contract: AppContract): PortalDeclaration | undefined;
|
package/dist/portal/alias.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readWebIntegration, resolvedWebLaunch, usesAuthGateway } from "../contract/web-launch.js";
|
|
3
2
|
/**
|
|
4
3
|
* DSM Application Portal alias: `https://<nas>/<alias>/` opens the App after
|
|
5
4
|
* DSM sign-in. DSM itself only refuses empty aliases (4100), synoman
|
|
@@ -16,6 +15,12 @@ export const RESERVED_PORTAL_ALIASES = [
|
|
|
16
15
|
"dsfile", "indexdb", "mobile", "oauth", "scripts", "sharing", "ssdp", "synosdsjslib", "synohdpack", "webapi", "webman",
|
|
17
16
|
"calendar", "chatplus", "contacts", "drive", "file", "photo", "sso", "synologymeet",
|
|
18
17
|
];
|
|
18
|
+
/**
|
|
19
|
+
* A portal needs the executor tier: DSM removes the alias files on uninstall
|
|
20
|
+
* but leaves nginx serving the alias, and only the root executor purge can
|
|
21
|
+
* reload it. A no-privilege App never runs anything as root.
|
|
22
|
+
*/
|
|
23
|
+
export const PORTAL_REQUIRES_EXECUTOR = "portal 需要 executor 層級(capabilities.rootOps 或 delegate):解除安裝後 DSM 不會 reload nginx,登入入口別名要由 root executor 清除。沒有 executor 的 App 請移除 portal,改由管理員在 DSM 手動設定反向代理。";
|
|
19
24
|
export function isPortalAlias(value) {
|
|
20
25
|
return typeof value === "string" && PORTAL_ALIAS_PATTERN.test(value) && !RESERVED_PORTAL_ALIASES.includes(value);
|
|
21
26
|
}
|
|
@@ -36,13 +41,3 @@ export function readPortalDeclaration(value, packageId) {
|
|
|
36
41
|
return { alias: defaultPortalAlias(packageId) };
|
|
37
42
|
return isPortalAlias(alias) ? { alias } : undefined;
|
|
38
43
|
}
|
|
39
|
-
/** A Compose App reaches the portal only as a DSM window behind the DSM-authenticated gateway. */
|
|
40
|
-
export function contractPortalSupported(contract) {
|
|
41
|
-
return resolvedWebLaunch(contract) === "dsm-window" && usesAuthGateway(contract);
|
|
42
|
-
}
|
|
43
|
-
export function resolvedContractPortal(contract) {
|
|
44
|
-
const portal = readWebIntegration(contract)?.portal;
|
|
45
|
-
if (portal === undefined || !contractPortalSupported(contract))
|
|
46
|
-
return undefined;
|
|
47
|
-
return readPortalDeclaration(portal, String(contract.package ?? ""));
|
|
48
|
-
}
|
package/dist/portal/package.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Translate } from "../build-spk/wizard-i18n.js";
|
|
2
2
|
import type { InstallWizardStep } from "../build-spk/install-wizard.js";
|
|
3
|
-
import type { AppContract } from "../types.js";
|
|
4
3
|
/** Package payload paths DSM's app-portal worker copies to /usr/syno/etc/www/app.d at start. */
|
|
5
4
|
export declare const PORTAL_JSON_RELPATH = "etc/portal.json";
|
|
6
5
|
export declare const PORTAL_MUSTACHE_RELPATH = "etc/portal.mustache";
|
|
@@ -16,8 +15,6 @@ export interface PortalPackage {
|
|
|
16
15
|
/** Default alias; the wizard answer replaces it. */
|
|
17
16
|
alias: string;
|
|
18
17
|
}
|
|
19
|
-
/** A Compose portal opens the App's `.url` entry of ui/config, keyed by the package id. */
|
|
20
|
-
export declare function contractPortal(contract: AppContract): PortalPackage | undefined;
|
|
21
18
|
export declare function portalResource(): Record<string, unknown>;
|
|
22
19
|
/** Every field of DSM's own app.d files; the worker refuses a partial file (synopkg start 272). */
|
|
23
20
|
export declare function renderPortalJson(portal: PortalPackage): string;
|
package/dist/portal/package.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PORTAL_ALIAS_MAX, PORTAL_ALIAS_PATTERN, RESERVED_PORTAL_ALIASES
|
|
1
|
+
import { PORTAL_ALIAS_MAX, PORTAL_ALIAS_PATTERN, RESERVED_PORTAL_ALIASES } from "./alias.js";
|
|
2
2
|
/** Package payload paths DSM's app-portal worker copies to /usr/syno/etc/www/app.d at start. */
|
|
3
3
|
export const PORTAL_JSON_RELPATH = "etc/portal.json";
|
|
4
4
|
export const PORTAL_MUSTACHE_RELPATH = "etc/portal.mustache";
|
|
@@ -6,12 +6,6 @@ export const PORTAL_MUSTACHE_RELPATH = "etc/portal.mustache";
|
|
|
6
6
|
export const PORTAL_INSTALL_KEY = "portal_alias";
|
|
7
7
|
/** Package-owned install draft, never DSM's saved portal configuration. */
|
|
8
8
|
export const PORTAL_DRAFT = "portal-install.json";
|
|
9
|
-
/** A Compose portal opens the App's `.url` entry of ui/config, keyed by the package id. */
|
|
10
|
-
export function contractPortal(contract) {
|
|
11
|
-
const portal = resolvedContractPortal(contract);
|
|
12
|
-
const packageId = String(contract.package);
|
|
13
|
-
return portal && { appId: packageId, application: String(contract.displayname ?? packageId), alias: portal.alias };
|
|
14
|
-
}
|
|
15
9
|
export function portalResource() {
|
|
16
10
|
return { "web-config": { "app-portal": { "json-relpath": PORTAL_JSON_RELPATH, "mustache-relpath": PORTAL_MUSTACHE_RELPATH } } };
|
|
17
11
|
}
|
|
@@ -9,5 +9,16 @@
|
|
|
9
9
|
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
10
10
|
},
|
|
11
11
|
"goToolchain": "go1.26.9"
|
|
12
|
+
},
|
|
13
|
+
"0.9.1": {
|
|
14
|
+
"x86_64": "5d75af3ce1de5b1d3ef5d89e062be1651a8e8b84edbca9903eb1a0b81d122fb5",
|
|
15
|
+
"aarch64": "b0e1f8174bf53eb435616ae0faf39ac929b515056f0b2fad5a6c264a1ff7f6e2",
|
|
16
|
+
"armv7": "17db613a4bdb181e0aa0f127822bfc13d7473fa846aadfbac3344b7c17700a84",
|
|
17
|
+
"lifecycle": {
|
|
18
|
+
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
19
|
+
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
20
|
+
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
21
|
+
},
|
|
22
|
+
"goToolchain": "go1.26.9"
|
|
12
23
|
}
|
|
13
24
|
}
|
|
@@ -4,4 +4,10 @@ export interface UninstallCleanupItem {
|
|
|
4
4
|
condition: string;
|
|
5
5
|
action: string;
|
|
6
6
|
}
|
|
7
|
-
export
|
|
7
|
+
export interface UninstallCleanupOptions {
|
|
8
|
+
/** The App runs the root executor, which purges DSM's leftovers on uninstall. */
|
|
9
|
+
executor?: boolean;
|
|
10
|
+
/** The App declares a Login Portal alias; only the executor tier can. */
|
|
11
|
+
portal?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function uninstallCleanup(pkg: string, { executor, portal }?: UninstallCleanupOptions): UninstallCleanupItem[];
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
const AUTOMATIC_FAILED = 'Only if automatic cleanup failed after uninstall; see the system log entry synopkgland-executor.';
|
|
2
|
+
export function uninstallCleanup(pkg, { executor = false, portal = false } = {}) {
|
|
2
3
|
const items = [
|
|
3
4
|
{ target: `@appdata/${pkg}, @appconf/${pkg}, @apphome/${pkg}`, condition: 'After uninstall, if retained data or empty DSM directories are no longer needed.', action: 'Back up retained data; an administrator may remove only these package-specific directories. The delete-data checkbox empties their contents and preserves DSM directory roots.' },
|
|
4
5
|
{ target: `@appshare/${pkg}, /usr/syno/etc/packages/${pkg}`, condition: 'After uninstall, if DSM leaves package infrastructure or a package-config symlink.', action: 'Administrator: inspect these package-specific paths and remove unused infrastructure; do not follow the symlink to delete shared data.' },
|
|
6
|
+
// Package scripts run as the package account; only the root executor can edit these DSM files.
|
|
7
|
+
executor
|
|
8
|
+
? { target: `SYNO.SDS.${pkg}.Instance rules in Application Privileges`, condition: AUTOMATIC_FAILED, action: 'The root executor removes these rules automatically through the Application Privileges API on uninstall. Administrator: remove surviving rules in Control Panel > Application Privileges; DSM would apply them again to a later install under the same name.' }
|
|
9
|
+
: { target: `SYNO.SDS.${pkg}.Instance rules in Application Privileges`, condition: 'After uninstall, if this package will not be reinstalled.', action: 'DSM keeps these rules in /etc/synoappprivilege.db and applies them again to a later install under the same name. Administrator: remove them in Control Panel > Application Privileges.' },
|
|
10
|
+
...(executor && portal ? [{ target: `${pkg} Login Portal alias`, condition: AUTOMATIC_FAILED, action: 'DSM removes the alias configuration without reloading its web server; the root executor then reloads the web server automatically. Administrator: if the alias still opens a DSM page, save an application alias in Control Panel > Login Portal > Applications to reload it, or restart the NAS.' }] : []),
|
|
5
11
|
{ target: `${pkg} package account/group`, condition: 'After uninstall, only if this package will not be reinstalled and the identity is unused.', action: 'Administrator: review DSM internal account/group and associated ACLs before cleanup. Package scripts do not delete system identities.' },
|
|
6
12
|
];
|
|
7
13
|
if (executor)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REOPEN_EVENTS_RUNTIME } from "./reopen-events.js";
|
|
2
2
|
import { DSM_LANGS } from "./i18n.js";
|
|
3
|
-
import { PASSWORD_CONFIRM, ROOT_PANEL, ROOT_RUNTIME } from "./root-window.js";
|
|
3
|
+
import { PASSWORD_CONFIRM, ROOT_PANEL, ROOT_RUNTIME, rootPanelCatalogs } from "./root-window.js";
|
|
4
4
|
import { HOST_ADAPTER_RUNTIME } from "./host-adapter.js";
|
|
5
5
|
import { permitsOrdinaryUsers } from "./api-permissions.js";
|
|
6
6
|
import { PORTAL_RUNTIME } from "./portal-window.js";
|
|
@@ -297,21 +297,18 @@ ${ns}.webApi = function (win) {
|
|
|
297
297
|
}
|
|
298
298
|
const ROOT_PANEL_CSS = [
|
|
299
299
|
".dsmapp-rootpanel{margin:16px 20px;padding:12px 16px;border:1px solid #d5dce5;border-radius:4px;background:#f7f9fb;font-size:13px;line-height:1.5}",
|
|
300
|
-
".dsmapp-rootpanel h2{margin:0 0 6px;font-size:14px;font-weight:600}",
|
|
301
|
-
".dsmapp-rootpanel
|
|
302
|
-
".dsmapp-rootpanel dt{color:#526070}.dsmapp-rootpanel dd{margin:0}",
|
|
303
|
-
".dsmapp-rootpanel pre{margin:4px 0 8px;padding:6px 8px;background:#fff;border:1px solid #e1e6ec;white-space:pre-wrap;max-height:160px;overflow:auto}",
|
|
300
|
+
".dsmapp-rootpanel h2{margin:0 0 6px;font-size:14px;font-weight:600}.dsmapp-rootpanel p{margin:4px 0}",
|
|
301
|
+
".dsmapp-rootpanel label{display:block;margin:8px 0}.dsmapp-rootpanel input{margin-left:8px;padding:3px 6px}",
|
|
304
302
|
".dsmapp-rootpanel-actions button{margin-right:8px;padding:4px 14px}",
|
|
305
303
|
".dsmapp-rootpanel-primary{background:#057feb;color:#fff;border:1px solid #057feb;border-radius:3px}",
|
|
306
|
-
".dsmapp-rootpanel-
|
|
304
|
+
".dsmapp-rootpanel-hint{color:#526070;font-size:12px}.dsmapp-rootpanel [hidden]{display:none}",
|
|
307
305
|
].join("");
|
|
308
306
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* has no scope picker.
|
|
307
|
+
* A Vue app approves in the SDK's ExecutorSettings, which also carries an
|
|
308
|
+
* install-selected scope; the window panel serves plain-UI apps only.
|
|
312
309
|
*/
|
|
313
310
|
function settingsOnlyApproval(manifest) {
|
|
314
|
-
return manifest.ui.build !== undefined
|
|
311
|
+
return manifest.ui.build !== undefined;
|
|
315
312
|
}
|
|
316
313
|
/**
|
|
317
314
|
* The capabilities.rootOps: true additions to the window: the exact tasks and panel
|
|
@@ -352,7 +349,7 @@ ${PASSWORD_CONFIRM}
|
|
|
352
349
|
});
|
|
353
350
|
${settingsOnlyApproval(manifest) ? "" : ` var panels = me.body && me.body.dom.querySelector(".dsmapp-rootpanels");
|
|
354
351
|
if (panels) {
|
|
355
|
-
${ns}.mountRootPanel(panels, access,
|
|
352
|
+
${ns}.mountRootPanel(panels, access, ${ns}.createI18n(lang, ${JSON.stringify(rootPanelCatalogs())}).t, session().admin);
|
|
356
353
|
}
|
|
357
354
|
`}`,
|
|
358
355
|
};
|
|
@@ -19,7 +19,7 @@ import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
|
|
|
19
19
|
import { renderStandaloneScript } from "./lifecycle.js";
|
|
20
20
|
import { listPortExposures, loadCompose } from "../compose/analyze.js";
|
|
21
21
|
import { uninstallCleanup } from "../runtime/uninstall-cleanup.js";
|
|
22
|
-
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH,
|
|
22
|
+
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep } from "../portal/package.js";
|
|
23
23
|
import { isPortalAlias } from "../portal/alias.js";
|
|
24
24
|
import { defaultPackageIcons, MAX_ICON_BYTES } from "../build-spk/dsm-icons.js";
|
|
25
25
|
import { decodePng } from "../build-spk/png.js";
|
|
@@ -70,7 +70,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
70
70
|
await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
|
|
71
71
|
const background = kind === "native-go" && paths.has(BACKGROUND_WORKER_PAYLOAD);
|
|
72
72
|
const sharedFolder = standalone ? recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))) : undefined;
|
|
73
|
-
|
|
73
|
+
// A Compose App has no executor, so its SPK never carries a portal.
|
|
74
|
+
const portal = standalone ? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings) : undefined;
|
|
74
75
|
if (portal && info.os_min_ver && OS_MIN_VER.test(info.os_min_ver) && !isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
|
|
75
76
|
findings.push(`INFO os_min_ver「${info.os_min_ver}」低於 portal 需要的 DSM ${PORTAL_MIN_DSM};DSM 7.1.1 以錯誤 320 拒裝帶 web-config app-portal 的未簽章套件,7.2 未驗證`);
|
|
76
77
|
}
|
|
@@ -80,6 +81,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
80
81
|
if (kind === "container")
|
|
81
82
|
await checkContainerExposure(dir, join(dir, "package.tgz"), findings);
|
|
82
83
|
const hasExecutor = (metadata.capabilities ?? []).some(name => name === "rootOps" || name === "delegate") || paths.has("executor/executor");
|
|
84
|
+
if (standalone && portal && !hasExecutor)
|
|
85
|
+
findings.push("PORTAL_REQUIRES_EXECUTOR portal 需要 executor 層級(rootOps 或 delegate):解除安裝後只有 root purge 能讓 nginx 停止提供別名");
|
|
83
86
|
const daemon = standalone ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background) : undefined;
|
|
84
87
|
if (background)
|
|
85
88
|
checkBackground(dir, info, payload, findings, hasExecutor, daemon);
|
|
@@ -104,7 +107,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
104
107
|
findings.push(`Executor scripts/${hook} differs from fixed lifecycle.`);
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
|
-
for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", { executor: true }))) {
|
|
110
|
+
for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", { executor: true, portal: Boolean(portal) }))) {
|
|
108
111
|
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
109
112
|
findings.push(`Executor WIZARD_UIFILES/${name} is missing cleanup guidance.`);
|
|
110
113
|
}
|
|
@@ -130,7 +133,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
130
133
|
...(executor ? { executor } : {}),
|
|
131
134
|
...(portal ? { portal: { appId: portal.appId, alias: portal.alias } } : {}),
|
|
132
135
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
133
|
-
...(standalone ? { uninstallCleanup: uninstallCleanup(info.package, hasExecutor) } : {}),
|
|
136
|
+
...(standalone ? { uninstallCleanup: uninstallCleanup(info.package, { executor: hasExecutor, portal: Boolean(portal) }) } : {}),
|
|
134
137
|
package: info.package,
|
|
135
138
|
version: info.version,
|
|
136
139
|
arch: info.arch,
|
|
@@ -324,11 +327,9 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal) {
|
|
|
324
327
|
if (!resource["docker-project"])
|
|
325
328
|
findings.push("container 缺少 docker-project resource");
|
|
326
329
|
for (const key of Object.keys(resource)) {
|
|
327
|
-
if (!["docker-project", "webservice"
|
|
330
|
+
if (!["docker-project", "webservice"].includes(key))
|
|
328
331
|
findings.push(`未知 container resource: ${key}`);
|
|
329
332
|
}
|
|
330
|
-
if (portal && JSON.stringify(resource["web-config"]) !== JSON.stringify(portalResource()["web-config"]))
|
|
331
|
-
findings.push("container web-config 必須只有固定的 app-portal 註冊");
|
|
332
333
|
}
|
|
333
334
|
catch {
|
|
334
335
|
findings.push("container 缺少有效的 conf/resource");
|
|
@@ -11,7 +11,7 @@ import { readBackground } from "./background.js";
|
|
|
11
11
|
import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_MIN_DSM, portalMinDsmMessage } from "../build-spk/dsm-version.js";
|
|
12
12
|
import { readSharedFolder } from "./shared-folder.js";
|
|
13
13
|
import { floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions } from "./api-permissions.js";
|
|
14
|
-
import { readPortalDeclaration } from "../portal/alias.js";
|
|
14
|
+
import { PORTAL_REQUIRES_EXECUTOR, readPortalDeclaration } from "../portal/alias.js";
|
|
15
15
|
import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
16
16
|
import { validateReproducible } from "../reproducible-config.js";
|
|
17
17
|
import { SDK_MODULE, sdkRequirement } from "../runtime/sdk-source.js";
|
|
@@ -137,7 +137,12 @@ function readManifest(raw, c) {
|
|
|
137
137
|
permissions = readApiPermissions(backend.dir, capabilities);
|
|
138
138
|
}
|
|
139
139
|
catch (error) {
|
|
140
|
-
|
|
140
|
+
// The SDK's own routes come from its module; a failed download is not a policy problem.
|
|
141
|
+
const download = /^\[SDK_DOWNLOAD_FAILED\] ([\s\S]*)$/.exec(error.message);
|
|
142
|
+
if (download)
|
|
143
|
+
c.fail("SDK_DOWNLOAD_FAILED", download[1]);
|
|
144
|
+
else
|
|
145
|
+
c.fail("STANDALONE_API_PERMISSIONS_INVALID", `backend/internal/api/permissions.json must define the centralized API policy: ${String(error)}`);
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
const daemon = arches && readDaemon(raw.daemon, c.root, arches, e => c.fail(e.code, e.message));
|
|
@@ -172,6 +177,9 @@ function readManifest(raw, c) {
|
|
|
172
177
|
const icon = raw.icon === undefined ? undefined : readIcon(raw.icon, c);
|
|
173
178
|
const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
|
|
174
179
|
?? c.fail("STANDALONE_PORTAL_INVALID", "portal 必須是 {} 或 { alias: <小寫英數與 -,1–64 字,不可為 DSM 保留路徑> }。");
|
|
180
|
+
if (portal && !capabilities.rootOps && !capabilities.delegate) {
|
|
181
|
+
c.fail("STANDALONE_PORTAL_REQUIRES_EXECUTOR", PORTAL_REQUIRES_EXECUTOR);
|
|
182
|
+
}
|
|
175
183
|
if (portal && osMinVer && !isDsmAtLeast(osMinVer, PORTAL_MIN_DSM)) {
|
|
176
184
|
c.fail("STANDALONE_PORTAL_MIN_DSM", portalMinDsmMessage(osMinVer, "portal"));
|
|
177
185
|
}
|
|
@@ -230,7 +238,8 @@ function readEntry(value, backend, policy, c) {
|
|
|
230
238
|
const main = backend.kind === "go" ? join(backend.dir, backend.main, "main.go") : "";
|
|
231
239
|
if (backend.kind !== "go" || !sdkRequirement(backend.dir)
|
|
232
240
|
|| !existsSync(main) || !readFileSync(main, "utf8").includes("dsm.NewEntryGate(")) {
|
|
233
|
-
|
|
241
|
+
const shown = main ? relative(c.root, main) : "backend.main 的 main.go";
|
|
242
|
+
return c.fail("STANDALONE_ENTRY_UNSUPPORTED", `entry 需要 SDK 的 entry gate:backend/go.mod 要 require ${SDK_MODULE},並在 ${shown} 以 dsm.NewEntryGate 包住 authenticator。`);
|
|
234
243
|
}
|
|
235
244
|
if (value === "app-privilege" && !permitsOrdinaryUsers(policy)) {
|
|
236
245
|
return c.fail("STANDALONE_ENTRY_INVALID", "entry: app-privilege 需要至少一個 permissions.json 標為 authenticated 的 route,一般使用者才有功能可用。");
|
|
@@ -2,7 +2,14 @@ export { type ExecutorTask as RootTask } from '../runtime/executor-tasks.js';
|
|
|
2
2
|
/** Exact readback is authoritative. PasswordConfirm tokens are reused immediately,
|
|
3
3
|
* across both Root APIs, and never persisted (DSM validity is about 300 seconds). */
|
|
4
4
|
export declare const ROOT_RUNTIME: string;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
|
|
5
|
+
type PanelLocale = "en" | "zh-TW" | "zh-CN" | "ja" | "ko" | "de" | "fr" | "es" | "it";
|
|
6
|
+
/** The plain-UI window panel's text; a state's reason stays the runtime's English detail. */
|
|
7
|
+
export declare const ROOT_PANEL_MESSAGES: Record<string, Record<PanelLocale, string>>;
|
|
8
|
+
/** ROOT_PANEL_MESSAGES as createI18n catalogs, one per locale. */
|
|
9
|
+
export declare function rootPanelCatalogs(): Record<string, Record<string, string>>;
|
|
10
|
+
/** The plain-UI approval panel: it asks for the password itself and passes it
|
|
11
|
+
* to `access.authorize`, the same PasswordConfirm path as ExecutorSettings. */
|
|
12
|
+
export declare const ROOT_PANEL = "function mountRootPanel(host, access, t, admin) {\n if (!admin) return;\n function el(tag, className) { var e = document.createElement(tag); if (className) e.className = className; return e; }\n var panel = el(\"div\", \"dsmapp-rootpanel\"), title = el(\"h2\"), status = el(\"p\"), reason = el(\"p\", \"dsmapp-rootpanel-hint\");\n var form = el(\"form\"), label = el(\"label\"), password = el(\"input\"), actions = el(\"div\", \"dsmapp-rootpanel-actions\");\n var approve = el(\"button\", \"dsmapp-rootpanel-primary\"), check = el(\"button\");\n title.textContent = t(\"title\"); label.textContent = t(\"password\");\n password.type = \"password\"; password.autocomplete = \"current-password\";\n approve.type = \"submit\"; approve.textContent = t(\"approve\");\n check.type = \"button\"; check.textContent = t(\"check\");\n label.appendChild(password); actions.appendChild(approve); actions.appendChild(check);\n form.appendChild(label); form.appendChild(actions);\n panel.appendChild(title); panel.appendChild(status); panel.appendChild(reason); panel.appendChild(form);\n host.appendChild(panel);\n function show(result) {\n status.textContent = t(\"state.\" + result.state);\n reason.textContent = result.reason || \"\"; reason.hidden = !result.reason;\n label.hidden = approve.hidden = result.state !== \"needs-admin-approval\" && result.retry !== true;\n }\n function run(fn) { approve.disabled = check.disabled = true; return fn().then(show, function(e) { show({state: \"blocked\", reason: e.message || e.code}); }).then(function() { approve.disabled = check.disabled = false; }); }\n access.subscribe(show);\n form.onsubmit = function(event) {\n if (event) event.preventDefault();\n var entered = password.value; password.value = \"\";\n if (!entered) return Promise.resolve();\n return run(function() { return access.authorize(entered); });\n };\n check.onclick = function() { return run(function() { return access.status(); }); };\n run(function() { return access.status().then(function(result) { return result.tasks && result.tasks.boot && result.tasks.control && result.tasks.boot.state === \"ready\" && result.tasks.control.state === \"ready\" ? access.control(\"start\") : result; }); });\n}";
|
|
13
|
+
/** Administrator password confirmation in the window's own session, for the
|
|
14
|
+
* password an approval panel collected. */
|
|
8
15
|
export declare const PASSWORD_CONFIRM: string;
|
|
@@ -236,20 +236,61 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
236
236
|
subscribe: function (listener) { listeners.push(listener); }
|
|
237
237
|
};
|
|
238
238
|
}`;
|
|
239
|
-
|
|
239
|
+
/** The plain-UI window panel's text; a state's reason stays the runtime's English detail. */
|
|
240
|
+
export const ROOT_PANEL_MESSAGES = {
|
|
241
|
+
title: { en: "Background access", "zh-TW": "背景存取", "zh-CN": "后台访问", ja: "バックグラウンドアクセス", ko: "백그라운드 액세스", de: "Hintergrundzugriff", fr: "Accès en arrière-plan", es: "Acceso en segundo plano", it: "Accesso in background" },
|
|
242
|
+
password: { en: "DSM administrator password", "zh-TW": "DSM 管理員密碼", "zh-CN": "DSM 管理员密码", ja: "DSM 管理者パスワード", ko: "DSM 관리자 비밀번호", de: "DSM-Administratorkennwort", fr: "Mot de passe administrateur DSM", es: "Contraseña de administrador de DSM", it: "Password amministratore DSM" },
|
|
243
|
+
approve: { en: "Approve", "zh-TW": "核准", "zh-CN": "批准", ja: "承認", ko: "승인", de: "Genehmigen", fr: "Approuver", es: "Aprobar", it: "Approva" },
|
|
244
|
+
check: { en: "Check status", "zh-TW": "檢查狀態", "zh-CN": "检查状态", ja: "状態を確認", ko: "상태 확인", de: "Status prüfen", fr: "Vérifier l'état", es: "Comprobar estado", it: "Verifica stato" },
|
|
245
|
+
"state.ready": { en: "Approved and running.", "zh-TW": "已核准,正在執行。", "zh-CN": "已批准,正在运行。", ja: "承認済みで、実行中です。", ko: "승인되었으며 실행 중입니다.", de: "Genehmigt und aktiv.", fr: "Approuvé et actif.", es: "Aprobado y en ejecución.", it: "Approvato e in esecuzione." },
|
|
246
|
+
"state.needs-admin-approval": { en: "Needs administrator approval.", "zh-TW": "需要管理員核准。", "zh-CN": "需要管理员批准。", ja: "管理者の承認が必要です。", ko: "관리자 승인이 필요합니다.", de: "Genehmigung durch einen Administrator erforderlich.", fr: "Approbation d'un administrateur requise.", es: "Requiere la aprobación de un administrador.", it: "Richiede l'approvazione di un amministratore." },
|
|
247
|
+
"state.blocked": { en: "Not available.", "zh-TW": "目前無法使用。", "zh-CN": "目前无法使用。", ja: "現在は利用できません。", ko: "현재 사용할 수 없습니다.", de: "Derzeit nicht verfügbar.", fr: "Indisponible pour le moment.", es: "No disponible por ahora.", it: "Non disponibile al momento." },
|
|
248
|
+
"state.unsupported": { en: "This DSM does not support background access.", "zh-TW": "這台 DSM 不支援背景存取。", "zh-CN": "此 DSM 不支持后台访问。", ja: "この DSM はバックグラウンドアクセスに対応していません。", ko: "이 DSM은 백그라운드 액세스를 지원하지 않습니다.", de: "Dieses DSM unterstützt keinen Hintergrundzugriff.", fr: "Ce DSM ne prend pas en charge l'accès en arrière-plan.", es: "Este DSM no admite el acceso en segundo plano.", it: "Questo DSM non supporta l'accesso in background." },
|
|
249
|
+
"state.cancelled": { en: "Approval was cancelled.", "zh-TW": "已取消核准。", "zh-CN": "已取消批准。", ja: "承認はキャンセルされました。", ko: "승인이 취소되었습니다.", de: "Genehmigung abgebrochen.", fr: "Approbation annulée.", es: "Aprobación cancelada.", it: "Approvazione annullata." },
|
|
250
|
+
};
|
|
251
|
+
/** ROOT_PANEL_MESSAGES as createI18n catalogs, one per locale. */
|
|
252
|
+
export function rootPanelCatalogs() {
|
|
253
|
+
const out = {};
|
|
254
|
+
for (const [key, texts] of Object.entries(ROOT_PANEL_MESSAGES)) {
|
|
255
|
+
for (const [locale, text] of Object.entries(texts))
|
|
256
|
+
(out[locale] ??= {})[key] = text;
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
/** The plain-UI approval panel: it asks for the password itself and passes it
|
|
261
|
+
* to `access.authorize`, the same PasswordConfirm path as ExecutorSettings. */
|
|
262
|
+
export const ROOT_PANEL = `function mountRootPanel(host, access, t, admin) {
|
|
240
263
|
if (!admin) return;
|
|
241
|
-
var
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
264
|
+
function el(tag, className) { var e = document.createElement(tag); if (className) e.className = className; return e; }
|
|
265
|
+
var panel = el("div", "dsmapp-rootpanel"), title = el("h2"), status = el("p"), reason = el("p", "dsmapp-rootpanel-hint");
|
|
266
|
+
var form = el("form"), label = el("label"), password = el("input"), actions = el("div", "dsmapp-rootpanel-actions");
|
|
267
|
+
var approve = el("button", "dsmapp-rootpanel-primary"), check = el("button");
|
|
268
|
+
title.textContent = t("title"); label.textContent = t("password");
|
|
269
|
+
password.type = "password"; password.autocomplete = "current-password";
|
|
270
|
+
approve.type = "submit"; approve.textContent = t("approve");
|
|
271
|
+
check.type = "button"; check.textContent = t("check");
|
|
272
|
+
label.appendChild(password); actions.appendChild(approve); actions.appendChild(check);
|
|
273
|
+
form.appendChild(label); form.appendChild(actions);
|
|
274
|
+
panel.appendChild(title); panel.appendChild(status); panel.appendChild(reason); panel.appendChild(form);
|
|
275
|
+
host.appendChild(panel);
|
|
276
|
+
function show(result) {
|
|
277
|
+
status.textContent = t("state." + result.state);
|
|
278
|
+
reason.textContent = result.reason || ""; reason.hidden = !result.reason;
|
|
279
|
+
label.hidden = approve.hidden = result.state !== "needs-admin-approval" && result.retry !== true;
|
|
280
|
+
}
|
|
281
|
+
function run(fn) { approve.disabled = check.disabled = true; return fn().then(show, function(e) { show({state: "blocked", reason: e.message || e.code}); }).then(function() { approve.disabled = check.disabled = false; }); }
|
|
246
282
|
access.subscribe(show);
|
|
247
|
-
|
|
248
|
-
|
|
283
|
+
form.onsubmit = function(event) {
|
|
284
|
+
if (event) event.preventDefault();
|
|
285
|
+
var entered = password.value; password.value = "";
|
|
286
|
+
if (!entered) return Promise.resolve();
|
|
287
|
+
return run(function() { return access.authorize(entered); });
|
|
288
|
+
};
|
|
289
|
+
check.onclick = function() { return run(function() { return access.status(); }); };
|
|
249
290
|
run(function() { return access.status().then(function(result) { return result.tasks && result.tasks.boot && result.tasks.control && result.tasks.boot.state === "ready" && result.tasks.control.state === "ready" ? access.control("start") : result; }); });
|
|
250
291
|
}`;
|
|
251
|
-
/** Administrator password confirmation in the window's own session
|
|
252
|
-
* password
|
|
292
|
+
/** Administrator password confirmation in the window's own session, for the
|
|
293
|
+
* password an approval panel collected. */
|
|
253
294
|
export const PASSWORD_CONFIRM = `function createPasswordConfirm(win) {
|
|
254
295
|
var versionUnsupported = ${versionUnsupported.toString()};
|
|
255
296
|
function send(password, version) {
|
|
@@ -270,24 +311,8 @@ export const PASSWORD_CONFIRM = `function createPasswordConfirm(win) {
|
|
|
270
311
|
return send(password, 1);
|
|
271
312
|
});
|
|
272
313
|
}
|
|
273
|
-
return function (
|
|
274
|
-
if (typeof
|
|
275
|
-
return
|
|
276
|
-
var form = document.createElement("form"), label = document.createElement("label"), input = document.createElement("input");
|
|
277
|
-
var submit = document.createElement("button"), cancel = document.createElement("button");
|
|
278
|
-
label.textContent = "Confirm your DSM administrator password";
|
|
279
|
-
input.type = "password"; input.autocomplete = "current-password"; input.required = true;
|
|
280
|
-
label.appendChild(input); form.appendChild(label);
|
|
281
|
-
submit.type = "submit"; submit.textContent = "Approve"; form.appendChild(submit);
|
|
282
|
-
cancel.type = "button"; cancel.textContent = "Cancel"; form.appendChild(cancel);
|
|
283
|
-
win.body.dom.appendChild(form); input.focus();
|
|
284
|
-
function close() { input.value = ""; if (form.parentNode) form.parentNode.removeChild(form); }
|
|
285
|
-
cancel.onclick = function () { close(); resolve({confirmed: false}); };
|
|
286
|
-
form.onsubmit = function (event) {
|
|
287
|
-
event.preventDefault(); submit.disabled = cancel.disabled = true;
|
|
288
|
-
var password = input.value; input.value = "";
|
|
289
|
-
auth(password).then(function (result) { close(); resolve(result); }, function (error) { close(); reject(error); });
|
|
290
|
-
};
|
|
291
|
-
});
|
|
314
|
+
return function (password) {
|
|
315
|
+
if (typeof password !== "string" || !password) return Promise.reject({code: "password_required"});
|
|
316
|
+
return auth(password);
|
|
292
317
|
};
|
|
293
318
|
}`;
|
package/dist/standalone/spk.js
CHANGED
|
@@ -130,7 +130,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
130
130
|
...install.static,
|
|
131
131
|
...install.dynamic,
|
|
132
132
|
...(folder ? renderSharedFolderUpgradeWizardFiles(manifest.package, folder) : {}),
|
|
133
|
-
...renderUninstallWizardFiles(manifest.package, "native", { executor: Boolean(options.executor) }),
|
|
133
|
+
...renderUninstallWizardFiles(manifest.package, "native", { executor: Boolean(options.executor), portal: Boolean(portal) }),
|
|
134
134
|
});
|
|
135
135
|
writePackageCenterIcons(staging, icons);
|
|
136
136
|
mkdirSync(dirname(options.outputPath), { recursive: true });
|
package/dist/validate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { contractSpan, makeError } from "./errors.js";
|
|
4
|
+
import { getErrorCode } from "./error-registry.js";
|
|
4
5
|
import { collectInstallParameters, getValuePosition, loadContract } from "./contract/load.js";
|
|
5
6
|
import { validateContract } from "./contract/validate.js";
|
|
6
7
|
import { findListingUnsupportedInstallTypes } from "./contract/unsupported-types.js";
|
|
@@ -30,7 +31,7 @@ export function validateProject(project, options = {}) {
|
|
|
30
31
|
const loaded = isNativeLayout(project.root) ? loadNativeLayout(project.root) : loadStandaloneManifest(project.root);
|
|
31
32
|
return loaded.ok ? { ok: true, errors: [] } : { ok: false, errors: loaded.errors.map(error => ({
|
|
32
33
|
...error, span: { file: DSM_PACKAGE_FILENAME, line: 1, column: 1 },
|
|
33
|
-
suggestion: "請修正 dsm-package.yaml 的 schema 2 欄位與能力宣告。",
|
|
34
|
+
suggestion: getErrorCode(error.code)?.suggestion ?? "請修正 dsm-package.yaml 的 schema 2 欄位與能力宣告。",
|
|
34
35
|
})) };
|
|
35
36
|
}
|
|
36
37
|
const parsed = loadContract(project.contractPath);
|