@synopackageland/cli 0.14.1 → 0.15.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 +20 -1
- package/README.md +33 -18
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -846
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/unsupported-types.js +1 -1
- package/dist/contract/validate.js +2 -2
- package/dist/contract/web-launch.d.ts +1 -12
- package/dist/contract/web-launch.js +10 -74
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/e2e/dsm.d.ts +2 -2
- package/dist/e2e/dsm.js +10 -6
- package/dist/error-registry.js +1020 -834
- package/dist/host/share-permission-lookup.d.ts +1 -1
- package/dist/host/share-permission.d.ts +1 -1
- package/dist/host/share-permission.js +1 -1
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +5 -5
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +39 -8
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +12 -0
- package/dist/standalone/inspect-spk.js +666 -136
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +8 -1
- package/dist/standalone/manifest.js +303 -86
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +138 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +4 -2
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +9 -18
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +5 -1
- package/skills/reference/identity-gateway.md +8 -144
- package/skills/reference/wrap.md +10 -39
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { strictJson } from './json.js';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import { CtlError } from './errors.js';
|
|
5
|
+
export const APP_COOKIE = Symbol("ctlAppCookie");
|
|
6
|
+
export function canonicalOrigin(input, tunnel = false) {
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(input);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new CtlError('CTL_URL_INVALID', 'A NAS origin URL is required.', 'run: synopkgland ctl host add <name> --url https://<nas>:5001');
|
|
13
|
+
}
|
|
14
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || !(url.protocol === 'https:' || (tunnel && url.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(url.hostname))))
|
|
15
|
+
throw new CtlError('CTL_URL_INVALID', 'Use HTTPS, or explicit --ssh-tunnel for a loopback HTTP tunnel.', 'run: synopkgland ctl host add <name> --url https://<nas>:5001');
|
|
16
|
+
return url.origin;
|
|
17
|
+
}
|
|
18
|
+
export function createRequest(origin, timeoutMs = 10000, ca) {
|
|
19
|
+
return async (path, body, session) => {
|
|
20
|
+
if (!path.startsWith('/') || path.startsWith('//') || path.includes('?') || path.includes('#') || path.includes('\\') || path.includes('%') || path.split('/').some(p => p === '.' || p === '..'))
|
|
21
|
+
throw new CtlError('CTL_PATH_INVALID', 'Unsafe request path.', 'run: synopkgland ctl list --refresh');
|
|
22
|
+
const url = new URL(path, origin);
|
|
23
|
+
if (url.origin !== origin || (session && session.origin !== origin))
|
|
24
|
+
throw new CtlError('CTL_ORIGIN_MISMATCH', 'Credentials cannot cross NAS origins.', 'run: synopkgland ctl host list', 4);
|
|
25
|
+
const form = body instanceof URLSearchParams;
|
|
26
|
+
const payload = Buffer.from(form ? body.toString() : JSON.stringify(body));
|
|
27
|
+
if (payload.length > 1048576)
|
|
28
|
+
throw new CtlError('CTL_INPUT_TOO_LARGE', 'Request exceeds 1 MiB.', 'Reduce the input.');
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const headers = { 'content-type': form ? 'application/x-www-form-urlencoded' : 'application/json', accept: 'application/json', 'content-length': String(payload.length), origin };
|
|
31
|
+
if (session) {
|
|
32
|
+
headers.cookie = session.cookie;
|
|
33
|
+
headers['X-SYNO-TOKEN'] = session.token;
|
|
34
|
+
}
|
|
35
|
+
const req = (url.protocol === 'https:' ? https : http).request(url, { method: 'POST', headers, timeout: timeoutMs, ...(ca ? { ca } : {}) }, res => {
|
|
36
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
37
|
+
res.resume();
|
|
38
|
+
reject(new CtlError('CTL_HTTP_ERROR', `HTTP ${res.statusCode}; redirects are never followed.`, 'Check the NAS URL and connection.', 6));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!res.headers['content-type']?.includes('application/json')) {
|
|
42
|
+
res.resume();
|
|
43
|
+
reject(new CtlError('CTL_RESPONSE_INVALID', 'NAS returned non-JSON data.', 'Check the endpoint and installed package.', 5));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const chunks = [];
|
|
47
|
+
let length = 0;
|
|
48
|
+
res.on('data', (c) => { length += c.length; if (length > 1048576) {
|
|
49
|
+
res.destroy();
|
|
50
|
+
reject(new CtlError('CTL_RESPONSE_TOO_LARGE', 'Response exceeds 1 MiB.', 'Use a smaller operation.', 5));
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
chunks.push(c); });
|
|
54
|
+
res.on('error', () => reject(new CtlError('CTL_NETWORK', 'Response interrupted.', 'Check the NAS connection; state-changing requests are not replayed.', 6)));
|
|
55
|
+
res.on('end', () => {
|
|
56
|
+
try {
|
|
57
|
+
const body = strictJson(Buffer.concat(chunks));
|
|
58
|
+
if (!form && body?.success === true && body.data && path.startsWith('/webman/3rdparty/') && url.protocol === 'https:') {
|
|
59
|
+
const cookies = (res.headers['set-cookie'] ?? []).filter(line => /;\s*Secure(?:;|$)/i.test(line) && /;\s*HttpOnly(?:;|$)/i.test(line) && !/;\s*Domain=/i.test(line));
|
|
60
|
+
const own = path.slice(0, path.lastIndexOf('/cgi/')) + '/';
|
|
61
|
+
const valid = cookies.filter(line => { const match = /;\s*Path=([^;]+)/i.exec(line); return match?.[1] === own; }).map(line => line.split(';')[0]).filter(pair => !pair.startsWith('id='));
|
|
62
|
+
if (valid.length)
|
|
63
|
+
Object.defineProperty(body, APP_COOKIE, { value: valid.join('; '), enumerable: false });
|
|
64
|
+
}
|
|
65
|
+
resolve(body);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
reject(new CtlError('CTL_RESPONSE_INVALID', 'Invalid JSON response.', 'Check the installed package.', 5));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const wallTimer = setTimeout(() => req.destroy(), timeoutMs);
|
|
73
|
+
req.once('close', () => clearTimeout(wallTimer));
|
|
74
|
+
req.on('timeout', () => req.destroy());
|
|
75
|
+
req.on('error', () => reject(new CtlError('CTL_NETWORK', 'NAS request failed or timed out.', 'Check the NAS connection and trusted CA; requests are not replayed.', 6)));
|
|
76
|
+
req.end(payload);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/deploy/deploy.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CommandRunner } from "../standalone/build.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
3
3
|
import type { ProjectRoot } from "../discovery.js";
|
|
4
4
|
import type { NasHost } from "../host/types.js";
|
|
5
|
-
import { resolveDeploySpkVersion } from "./spk-version.js";
|
|
5
|
+
import type { resolveDeploySpkVersion } from "./spk-version.js";
|
|
6
6
|
export interface DeployProjectOptions {
|
|
7
7
|
project: ProjectRoot;
|
|
8
8
|
/** Gateway architecture. Omitted means "ask the NAS" when needed. */
|
|
@@ -27,4 +27,5 @@ export interface DeployProjectResult {
|
|
|
27
27
|
/** Non-blocking pre-flight remarks, e.g. a read-only Shared Folder grant. */
|
|
28
28
|
notes?: string[];
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
/** Removed legacy dispatch; schema-2 deployment uses planDeployment/deployStandalone. */
|
|
31
|
+
export declare function deployProject(_options: DeployProjectOptions): Promise<DeployProjectResult>;
|
package/dist/deploy/deploy.js
CHANGED
|
@@ -1,213 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { loadContract } from "../contract/load.js";
|
|
4
|
-
import { usesAuthGateway } from "../contract/web-launch.js";
|
|
5
|
-
import { findUnsupportedInstallTypes } from "../contract/unsupported-types.js";
|
|
6
|
-
import { appOriginFromContract } from "../host/open-url.js";
|
|
7
|
-
import { validateProject } from "../validate.js";
|
|
8
|
-
import { resolveDeployIdentity, PACKAGE_IDENTITY_MAX_LENGTH } from "./identity.js";
|
|
9
|
-
import { resolveDeploySpkVersion } from "./spk-version.js";
|
|
10
|
-
import { resolveSharedFolderExtraValues, sharedFolderParameterNames } from "./share-values.js";
|
|
11
|
-
import { resolveDsmUserExtraValues, dsmUserParameterNames } from "./run-as-values.js";
|
|
12
|
-
import { runSharePermissionPreflight } from "./share-permission-preflight.js";
|
|
13
|
-
import { homesParameterNames, resolveHomesExtraValues } from "./homes-values.js";
|
|
14
|
-
import { resolveInstallParameterExtraValues } from "./param-values.js";
|
|
15
|
-
import { loadCompose } from "../compose/analyze.js";
|
|
16
|
-
import { runPortPreflight } from "./port-preflight.js";
|
|
17
|
-
const POSIX_ALL_SHARES_LOOKUP_FAILED = "DEPLOY_POSIX_ALL_SHARES_LOOKUP_FAILED";
|
|
18
|
-
export async function deployProject(options) {
|
|
19
|
-
const contract = loadContract(options.project.contractPath).contract;
|
|
20
|
-
// A45/A46: refuse before anything is built or uploaded. validate only warns,
|
|
21
|
-
// so this is the gate that keeps an uninstallable App off a NAS.
|
|
22
|
-
const unsupported = findUnsupportedInstallTypes(contract);
|
|
23
|
-
if (unsupported.length > 0) {
|
|
24
|
-
const first = unsupported[0];
|
|
25
|
-
return {
|
|
26
|
-
ok: false,
|
|
27
|
-
code: first.code,
|
|
28
|
-
message: `The Install Parameter "${first.name}" with type: ${first.type} has no usable install path today, ` +
|
|
29
|
-
"a Package Center install ends in DSM broken/150, so deploy refuses to deploy." +
|
|
30
|
-
(first.type === "homes"
|
|
31
|
-
? "If you need an administrator-chosen folder, use type: shared_folder."
|
|
32
|
-
: "Use type: string, or have the App read the file from ${SYNO_APP_SECRETS_DIR}."),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const validation = validateProject(options.project);
|
|
36
|
-
if (!validation.ok) {
|
|
37
|
-
return { ok: false, code: "VALIDATION_FAILED", message: validation.errors.map((e) => e.code).join(", ") };
|
|
38
|
-
}
|
|
39
|
-
const identity = resolveDeployIdentity(options.project, { dev: options.dev });
|
|
40
|
-
if (identity.packageId.length > PACKAGE_IDENTITY_MAX_LENGTH) {
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
code: "PACKAGE_IDENTITY_TOO_LONG",
|
|
44
|
-
message: `The final Package Identity "${identity.packageId}" length ${identity.packageId.length} exceeds the DSM limit of ${PACKAGE_IDENTITY_MAX_LENGTH} characters.`,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const packageInstalled = await options.host.isPackageInstalled(identity.packageId);
|
|
48
|
-
const explicitParamNames = new Set(Object.keys(options.paramValues ?? {}));
|
|
49
|
-
const paramValues = { ...(options.paramValues ?? {}) };
|
|
50
|
-
const posixAllSharesNames = Object.entries(contract.install ?? {})
|
|
51
|
-
.filter(([, parameter]) => parameter?.type === "posix_all_shares")
|
|
52
|
-
.map(([name]) => name);
|
|
53
|
-
for (const name of posixAllSharesNames) {
|
|
54
|
-
if (Object.prototype.hasOwnProperty.call(paramValues, name)) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
let shareNames;
|
|
58
|
-
try {
|
|
59
|
-
shareNames = await options.host.listShareNames?.();
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
shareNames = undefined;
|
|
63
|
-
}
|
|
64
|
-
const names = (shareNames ?? []).map((value) => value.trim()).filter(Boolean);
|
|
65
|
-
if (names.length === 0) {
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
code: POSIX_ALL_SHARES_LOOKUP_FAILED,
|
|
69
|
-
message: `The Shared Folders of this NAS could not be listed with an administrator session, so the POSIX all-shares parameter "${name}" cannot be filled.`,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
paramValues[name] = names.join(",");
|
|
73
|
-
}
|
|
74
|
-
const paramResolved = resolveInstallParameterExtraValues({
|
|
75
|
-
contract,
|
|
76
|
-
provided: paramValues,
|
|
77
|
-
});
|
|
78
|
-
if (!paramResolved.ok) {
|
|
79
|
-
return { ok: false, code: paramResolved.code, message: paramResolved.message };
|
|
80
|
-
}
|
|
81
|
-
const homesResolved = await resolveHomesExtraValues({
|
|
82
|
-
requiredKeys: homesParameterNames(contract),
|
|
83
|
-
lookup: async () => options.host.resolveHomesPath?.(),
|
|
84
|
-
});
|
|
85
|
-
if (!homesResolved.ok) {
|
|
86
|
-
return { ok: false, code: homesResolved.code, message: homesResolved.message };
|
|
87
|
-
}
|
|
88
|
-
const shareResolved = await resolveSharedFolderExtraValues({
|
|
89
|
-
requiredKeys: sharedFolderParameterNames(contract).filter((key) => !packageInstalled || options.shareValues?.[key] !== undefined),
|
|
90
|
-
provided: options.shareValues ?? {},
|
|
91
|
-
lookup: async (logicalName) => options.host.resolveSharePath?.(logicalName),
|
|
92
|
-
});
|
|
93
|
-
if (!shareResolved.ok) {
|
|
94
|
-
return { ok: false, code: shareResolved.code, message: shareResolved.message };
|
|
95
|
-
}
|
|
96
|
-
const runAsResolved = resolveDsmUserExtraValues({
|
|
97
|
-
requiredKeys: dsmUserParameterNames(contract).filter((key) => !packageInstalled || options.runAsValues?.[key] !== undefined),
|
|
98
|
-
provided: options.runAsValues ?? {},
|
|
99
|
-
});
|
|
100
|
-
if (!runAsResolved.ok) {
|
|
101
|
-
return { ok: false, code: runAsResolved.code, message: runAsResolved.message };
|
|
102
|
-
}
|
|
103
|
-
// A42 pre-flight: preinst runs as the package user and can never read ACL, so
|
|
104
|
-
// the only place that can catch "selected user cannot reach selected Share" is
|
|
105
|
-
// here, where we still hold an administrator session. Refuses before install.
|
|
106
|
-
const sharePermission = await runSharePermissionPreflight({
|
|
107
|
-
shares: Object.fromEntries(sharedFolderParameterNames(contract)
|
|
108
|
-
.map((key) => [key, (options.shareValues ?? {})[key] ?? ""])
|
|
109
|
-
.filter(([, value]) => value.trim() !== "")),
|
|
110
|
-
runAsUsers: runAsResolved.extraValues,
|
|
111
|
-
listSharePermissions: options.host.listSharePermissions
|
|
112
|
-
? (name) => options.host.listSharePermissions(name)
|
|
113
|
-
: undefined,
|
|
114
|
-
listUserGroups: options.host.listUserGroups
|
|
115
|
-
? (name) => options.host.listUserGroups(name)
|
|
116
|
-
: undefined,
|
|
117
|
-
});
|
|
118
|
-
if (!sharePermission.ok) {
|
|
119
|
-
return { ok: false, code: sharePermission.code, message: sharePermission.message };
|
|
120
|
-
}
|
|
121
|
-
const portPreflight = await runPortPreflight({
|
|
122
|
-
compose: loadCompose(options.project.composePath)?.compose,
|
|
123
|
-
packageIdentity: identity.packageId,
|
|
124
|
-
listPortInfo: options.host.listPortInfo
|
|
125
|
-
? () => options.host.listPortInfo()
|
|
126
|
-
: undefined,
|
|
127
|
-
});
|
|
128
|
-
if (!portPreflight.ok) {
|
|
129
|
-
return { ok: false, code: portPreflight.code, message: portPreflight.message };
|
|
130
|
-
}
|
|
131
|
-
const extraValues = {
|
|
132
|
-
...paramResolved.extraValues,
|
|
133
|
-
...homesResolved.extraValues,
|
|
134
|
-
...shareResolved.extraValues,
|
|
135
|
-
...runAsResolved.extraValues,
|
|
136
|
-
};
|
|
137
|
-
// The upgrade wizard deliberately ignores auto-filled Share and Run-as controls unless
|
|
138
|
-
// the administrator opted into a change. Keep the CLI's explicit upgrade
|
|
139
|
-
// flags equivalent to checking those controls.
|
|
140
|
-
if (packageInstalled) {
|
|
141
|
-
for (const name of sharedFolderParameterNames(contract)) {
|
|
142
|
-
if ((options.shareValues?.[name] ?? "").trim() !== "") {
|
|
143
|
-
extraValues[`CHANGE_${name}`] = "true";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
for (const name of dsmUserParameterNames(contract)) {
|
|
147
|
-
if ((options.runAsValues?.[name] ?? "").trim() !== "") {
|
|
148
|
-
extraValues[`CHANGE_${name}`] = "true";
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
for (const name of posixAllSharesNames) {
|
|
152
|
-
if (explicitParamNames.has(name) && (paramValues[name] ?? "").trim() !== "") {
|
|
153
|
-
extraValues[`CHANGE_${name}`] = "true";
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const resolveSpkVersion = options.spkVersionResolver ?? resolveDeploySpkVersion;
|
|
158
|
-
let spkVersion;
|
|
159
|
-
try {
|
|
160
|
-
spkVersion = await resolveSpkVersion(options.project, options.host, identity.packageId);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
return { ok: false, code: "DEPLOY_VERSION_INVALID",
|
|
164
|
-
message: error instanceof Error ? error.message : String(error) };
|
|
165
|
-
}
|
|
166
|
-
let archFamily = options.archFamily;
|
|
167
|
-
// A DSM-authenticated web App ships a native gateway CGI, so it is built per architecture too.
|
|
168
|
-
if (archFamily === undefined && usesAuthGateway(contract)) {
|
|
169
|
-
archFamily = await options.host.getArchFamily?.();
|
|
170
|
-
if (archFamily === undefined) {
|
|
171
|
-
return {
|
|
172
|
-
ok: false,
|
|
173
|
-
code: "NATIVE_ARCH_UNDETERMINED",
|
|
174
|
-
message: "The CPU architecture could not be determined from this NAS. Specify --arch x86_64 or --arch aarch64 explicitly.",
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (archFamily !== undefined && !isContainerArchFamily(archFamily)) {
|
|
179
|
-
return {
|
|
180
|
-
ok: false,
|
|
181
|
-
code: "CONTAINER_ARCH_UNSUPPORTED",
|
|
182
|
-
message: `Container Manager Apps support only ${CONTAINER_ARCH_FAMILIES.join(", ")}; DSM does not provide Container Manager for ${archFamily}.`,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
const build = await buildSpk({
|
|
186
|
-
project: options.project,
|
|
187
|
-
packageIdentity: identity.packageId,
|
|
188
|
-
displayname: identity.displayname,
|
|
189
|
-
spkVersion,
|
|
190
|
-
archFamily,
|
|
191
|
-
gatewayRun: options.gatewayRun,
|
|
192
|
-
});
|
|
193
|
-
if (!build.ok || !build.outputPath) {
|
|
194
|
-
return { ok: false, code: "BUILD_SPK_FAILED", message: build.errors.join(", ") };
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
await options.host.installFromLocalSpk({
|
|
198
|
-
packageId: identity.packageId,
|
|
199
|
-
version: spkVersion,
|
|
200
|
-
spkPath: build.outputPath,
|
|
201
|
-
extraValues,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
return { ok: false, code: "LOCAL_SPK_INSTALL_FAILED",
|
|
206
|
-
message: error instanceof Error ? error.message : String(error) };
|
|
207
|
-
}
|
|
1
|
+
/** Removed legacy dispatch; schema-2 deployment uses planDeployment/deployStandalone. */
|
|
2
|
+
export async function deployProject(_options) {
|
|
208
3
|
return {
|
|
209
|
-
ok:
|
|
210
|
-
|
|
211
|
-
|
|
4
|
+
ok: false,
|
|
5
|
+
code: "COMPOSE_LEGACY_MANIFEST_REMOVED",
|
|
6
|
+
message: "Use schema-2 dsm-package.yaml and the unified deployment plan; the legacy Compose installer is removed.",
|
|
212
7
|
};
|
|
213
8
|
}
|
package/dist/deploy/plan.js
CHANGED
|
@@ -3,7 +3,7 @@ import { loadContract } from "../contract/load.js";
|
|
|
3
3
|
import { validateProject } from "../validate.js";
|
|
4
4
|
import { findStandaloneRoot, loadStandaloneManifest, } from "../standalone/manifest.js";
|
|
5
5
|
import { buildStandalone } from "../standalone/build.js";
|
|
6
|
-
import { buildNativeLayout, isNativeLayout, loadNativeLayout } from "../standalone/native-layout.js";
|
|
6
|
+
import { buildNativeLayout, isNativeLayout, loadNativeLayout, } from "../standalone/native-layout.js";
|
|
7
7
|
import { resolveDeployIdentity } from "./identity.js";
|
|
8
8
|
import { nextDeployVersion } from "./spk-version.js";
|
|
9
9
|
import { resolveNativeInstallValues } from "./native-install-values.js";
|
|
@@ -29,17 +29,28 @@ export function planDeployment(root, options = {}) {
|
|
|
29
29
|
throw fail("STANDALONE_ARCH_NOT_DECLARED", `Architecture not declared: ${options.arch}; add it to dsm-package.yaml arches to deploy it`);
|
|
30
30
|
return {
|
|
31
31
|
root: standalone,
|
|
32
|
-
kind: m.
|
|
32
|
+
kind: m.compose
|
|
33
|
+
? "container"
|
|
34
|
+
: m.backend.kind === "binary"
|
|
35
|
+
? "binary"
|
|
36
|
+
: `native-${m.backend.kind}`,
|
|
33
37
|
packageId: m.package,
|
|
34
38
|
version: m.spkVersion,
|
|
35
39
|
arch: options.arch,
|
|
36
40
|
capabilities: Object.keys(m.capabilities),
|
|
37
41
|
transport: "dsm-local-spk",
|
|
38
|
-
notes: m.
|
|
42
|
+
notes: m.compose
|
|
39
43
|
? [
|
|
40
|
-
"
|
|
44
|
+
"Containers use the fixed package identity; they do not inherit signed-in File Station ACLs.",
|
|
45
|
+
...m.compose.plan.acceptance_blockers,
|
|
41
46
|
]
|
|
42
|
-
:
|
|
47
|
+
: m.capabilities.rootOps ||
|
|
48
|
+
m.capabilities.delegate ||
|
|
49
|
+
m.capabilities.webapi
|
|
50
|
+
? [
|
|
51
|
+
"Installation does not mean ready. Approve boot and control tasks once in the App window. An executor-only upgrade stays ready without approval (INFO anchors the executor digest); a policy digest change requires approval again.",
|
|
52
|
+
]
|
|
53
|
+
: [],
|
|
43
54
|
};
|
|
44
55
|
}
|
|
45
56
|
const project = findProjectRoot(root);
|
|
@@ -100,7 +111,11 @@ async function buildForDeploy(plan, arch, installed, options) {
|
|
|
100
111
|
const name = Object.keys(options.params ?? {})[0];
|
|
101
112
|
if (name)
|
|
102
113
|
throw fail("DEPLOY_PARAM_UNDECLARED", `${m.package} has no install wizard values; remove --param ${name}.`);
|
|
103
|
-
const { outputs } = await buildNativeLayout({
|
|
114
|
+
const { outputs } = await buildNativeLayout({
|
|
115
|
+
root: plan.root,
|
|
116
|
+
arches: [layoutArch],
|
|
117
|
+
run: options.run,
|
|
118
|
+
});
|
|
104
119
|
return { outputs, extraValues: {} };
|
|
105
120
|
}
|
|
106
121
|
const loaded = loadStandaloneManifest(plan.root);
|
|
@@ -109,7 +124,11 @@ async function buildForDeploy(plan, arch, installed, options) {
|
|
|
109
124
|
const values = resolveNativeInstallValues(loaded.manifest, options.params ?? {}, { installed });
|
|
110
125
|
if (!values.ok)
|
|
111
126
|
throw fail(values.code, values.message);
|
|
112
|
-
const { outputs } = await buildStandalone({
|
|
127
|
+
const { outputs } = await buildStandalone({
|
|
128
|
+
root: plan.root,
|
|
129
|
+
arches: [arch],
|
|
130
|
+
run: options.run,
|
|
131
|
+
});
|
|
113
132
|
return { outputs, extraValues: values.extraValues };
|
|
114
133
|
}
|
|
115
134
|
export async function deployStandalone(plan, host, options = {}) {
|
package/dist/dev.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { createDockerComposeRunner } from "./compose/runner.js";
|
|
3
2
|
/** Run the application's own Compose project locally. */
|
|
4
3
|
export async function runDev(options) {
|
|
5
|
-
if (!existsSync(options.project.composePath))
|
|
6
|
-
throw new Error('dev requires compose.yaml');
|
|
7
4
|
const runner = options.composeRunner ?? createDockerComposeRunner();
|
|
8
5
|
try {
|
|
9
6
|
await runner.up(options.project.root);
|
|
@@ -13,5 +10,9 @@ export async function runDev(options) {
|
|
|
13
10
|
await runner.down?.(options.project.root);
|
|
14
11
|
throw error;
|
|
15
12
|
}
|
|
16
|
-
return {
|
|
13
|
+
return {
|
|
14
|
+
close: async () => {
|
|
15
|
+
await runner.down?.(options.project.root);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
17
18
|
}
|
package/dist/e2e/dsm.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ type Page = ReturnType<NonNullable<ReturnType<typeof surfaceOf>>["page"]>;
|
|
|
17
17
|
type Locator = ReturnType<Page["locator"]>;
|
|
18
18
|
/** The DSM URL the browser opens: DSM_E2E_URL, else the local end of the SSH tunnel. */
|
|
19
19
|
export declare function dsmUrl(env?: Env): string;
|
|
20
|
-
/** ssh arguments forwarding
|
|
20
|
+
/** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
|
|
21
21
|
export declare function tunnelArgs(env?: Env): string[];
|
|
22
22
|
/** A loopback port nothing listens on now, for a tunnel that names none. */
|
|
23
23
|
export declare function freePort(): Promise<number>;
|
|
@@ -122,7 +122,7 @@ export declare function waitForDesktop(page?: Page): Promise<void>;
|
|
|
122
122
|
/**
|
|
123
123
|
* Closes every DSM notification card; returns how many were closed. DSM shows
|
|
124
124
|
* them a moment after the desktop (the admin's "Admin account security" card
|
|
125
|
-
* about 2 s later on
|
|
125
|
+
* about 2 s later on a DSM 7.4 test NAS), so it first waits up to
|
|
126
126
|
* DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
|
|
127
127
|
*/
|
|
128
128
|
export declare function dismissNotifications(page?: Page, env?: Env): Promise<number>;
|
package/dist/e2e/dsm.js
CHANGED
|
@@ -41,11 +41,14 @@ function forwards(env) {
|
|
|
41
41
|
...extra(env.DSM_E2E_TUNNEL_HTTPS_PORT, env.DSM_E2E_REMOTE_HTTPS_PORT, "443"),
|
|
42
42
|
];
|
|
43
43
|
}
|
|
44
|
-
/** ssh arguments forwarding
|
|
44
|
+
/** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
|
|
45
45
|
export function tunnelArgs(env = process.env) {
|
|
46
|
+
const target = env.DSM_E2E_SSH?.trim();
|
|
47
|
+
if (!target)
|
|
48
|
+
throw new Error("DSM_E2E_SSH is required to open a NAS tunnel; set it to user@nas.example (using your NAS host).");
|
|
46
49
|
return ["-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes",
|
|
47
50
|
...forwards(env).flatMap(([local, remote]) => ["-L", `127.0.0.1:${local}:127.0.0.1:${remote}`]),
|
|
48
|
-
|
|
51
|
+
target];
|
|
49
52
|
}
|
|
50
53
|
/** A loopback port nothing listens on now, for a tunnel that names none. */
|
|
51
54
|
export function freePort() {
|
|
@@ -80,12 +83,13 @@ export async function runWithTunnel(command, outer = process.env) {
|
|
|
80
83
|
throw new Error("runWithTunnel needs a command to run.");
|
|
81
84
|
const port = Number(outer.DSM_E2E_TUNNEL_PORT || await freePort());
|
|
82
85
|
const env = { ...outer, DSM_E2E_TUNNEL_PORT: String(port) };
|
|
86
|
+
const args = tunnelArgs(env);
|
|
83
87
|
const ports = forwards(env).map(([local]) => Number(local));
|
|
84
88
|
for (const used of ports) {
|
|
85
89
|
if (await portOpen(used))
|
|
86
90
|
throw new Error(`Port ${used} is already in use; name another DSM_E2E_TUNNEL_* port or close it before opening the tunnel.`);
|
|
87
91
|
}
|
|
88
|
-
const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh",
|
|
92
|
+
const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh", args, { stdio: ["ignore", "ignore", "inherit"] });
|
|
89
93
|
let sshExited = false;
|
|
90
94
|
ssh.once("exit", () => { sshExited = true; });
|
|
91
95
|
try {
|
|
@@ -93,7 +97,7 @@ export async function runWithTunnel(command, outer = process.env) {
|
|
|
93
97
|
for (const waiting of ports) {
|
|
94
98
|
while (!(await portOpen(waiting))) {
|
|
95
99
|
if (sshExited || Date.now() > deadline)
|
|
96
|
-
throw new Error(`The SSH tunnel to ${
|
|
100
|
+
throw new Error(`The SSH tunnel to ${args.at(-1)} did not open on port ${waiting}.`);
|
|
97
101
|
await new Promise(accept => setTimeout(accept, 100));
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -364,7 +368,7 @@ const notificationCloseSelector = ".v-notification-container .v-notification-pan
|
|
|
364
368
|
/**
|
|
365
369
|
* Closes every DSM notification card; returns how many were closed. DSM shows
|
|
366
370
|
* them a moment after the desktop (the admin's "Admin account security" card
|
|
367
|
-
* about 2 s later on
|
|
371
|
+
* about 2 s later on a DSM 7.4 test NAS), so it first waits up to
|
|
368
372
|
* DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
|
|
369
373
|
*/
|
|
370
374
|
export async function dismissNotifications(page = dsmPage(), env = process.env) {
|
|
@@ -454,7 +458,7 @@ async function restoredSessionLive(page) {
|
|
|
454
458
|
*/
|
|
455
459
|
export async function signIn(app, user = "admin", env = process.env) {
|
|
456
460
|
const url = dsmUrl(env);
|
|
457
|
-
const file = profilePath(env.DSM_E2E_TARGET || "
|
|
461
|
+
const file = profilePath(env.DSM_E2E_TARGET || "dsm", user, env);
|
|
458
462
|
const context = surface().context();
|
|
459
463
|
const profile = loadProfile(file, url);
|
|
460
464
|
if (profile)
|