@synopackageland/cli 0.13.1 → 0.15.0
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 +18 -0
- package/README.md +44 -1
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -850
- 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/app-launch.d.ts +11 -0
- package/dist/contract/app-launch.js +18 -0
- package/dist/contract/web-launch.d.ts +3 -13
- package/dist/contract/web-launch.js +14 -81
- 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/error-registry.js +1020 -831
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +1 -12
- 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 +55 -9
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +15 -0
- package/dist/standalone/inspect-spk.js +714 -128
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +10 -1
- package/dist/standalone/manifest.js +308 -87
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +139 -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/synology-app.schema.json +1 -1
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +34 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +26 -4
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +4 -0
- package/skills/reference/identity-gateway.md +8 -138
- package/skills/reference/portal.md +31 -0
- package/skills/reference/ui-vue.md +31 -0
- package/skills/reference/wrap.md +10 -30
- 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
|
@@ -1,39 +1,64 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isWebLaunchMode } from "../contract/web-launch.js";
|
|
2
|
+
import { validWebAPIProfiles, } from "../runtime/webapi-profile.js";
|
|
2
3
|
import { validateMetadata } from "../package-metadata.js";
|
|
3
|
-
import { validDelegate } from "../runtime/executor-policy.js";
|
|
4
|
+
import { validDelegate, } from "../runtime/executor-policy.js";
|
|
4
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
7
|
import { parse } from "yaml";
|
|
7
|
-
import { isNativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
8
|
-
import { checkGlossary, loadCatalogs, readInfoTranslations } from "./i18n.js";
|
|
9
|
-
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR } from "./root-ops.js";
|
|
8
|
+
import { isNativeArchFamily, } from "../build-spk/dsm-arch.js";
|
|
9
|
+
import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
|
|
10
|
+
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
|
|
10
11
|
import { readComposeApp } from "./compose.js";
|
|
11
12
|
import { readDaemon } from "./daemon.js";
|
|
12
|
-
import { readIdentityGateway } from "./identity-gateway.js";
|
|
13
|
+
import { readIdentityGateway, } from "./identity-gateway.js";
|
|
13
14
|
import { readUninstall } from "./uninstall.js";
|
|
14
15
|
import { readAppScripts } from "./app-scripts.js";
|
|
15
16
|
import { readBackground } from "./background.js";
|
|
16
|
-
import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
|
|
17
|
+
import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage, } from "../build-spk/dsm-version.js";
|
|
17
18
|
import { readSharedFolder } from "./shared-folder.js";
|
|
18
|
-
import { API_PERMISSIONS_FILE, floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions } from "./api-permissions.js";
|
|
19
|
-
import { PORTAL_REQUIRES_EXECUTOR, portalEntryProblem, readPortalDeclaration } from "../portal/alias.js";
|
|
19
|
+
import { API_PERMISSIONS_FILE, floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions, } from "./api-permissions.js";
|
|
20
|
+
import { PORTAL_REQUIRES_EXECUTOR, portalEntryProblem, readPortalDeclaration, } from "../portal/alias.js";
|
|
20
21
|
import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
21
|
-
import { LOGIN_COLOR, loginImageProblem } from "../build-spk/login-style.js";
|
|
22
|
+
import { LOGIN_COLOR, loginImageProblem, } from "../build-spk/login-style.js";
|
|
22
23
|
import { validateReproducible } from "../reproducible-config.js";
|
|
23
|
-
import { appSdkVersion, SDK_MODULE } from "../runtime/sdk-source.js";
|
|
24
|
+
import { appSdkVersion, SDK_MODULE, } from "../runtime/sdk-source.js";
|
|
24
25
|
import { sdkProblems } from "./sdk-check.js";
|
|
25
26
|
import { readWindowSize } from "./window-size.js";
|
|
26
27
|
import { appSdkTimeouts } from "../runtime/sdk-timeouts.js";
|
|
27
|
-
import { requestTimeoutSeconds, rootRunDeadlineProblem, TIMEOUT_USAGE, validTimeoutSeconds } from "./request-timeout.js";
|
|
28
|
-
import { BRIDGE_RUNTIMES, isBridgeBackend, overlaps, readBridgeBackend } from "./bridge-backend.js";
|
|
28
|
+
import { requestTimeoutSeconds, rootRunDeadlineProblem, TIMEOUT_USAGE, validTimeoutSeconds, } from "./request-timeout.js";
|
|
29
|
+
import { BRIDGE_RUNTIMES, isBridgeBackend, overlaps, readBridgeBackend, } from "./bridge-backend.js";
|
|
29
30
|
/**
|
|
30
31
|
* `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
|
|
31
32
|
* runs as its own DSM package account with a CGI backend and a DSM window UI.
|
|
32
33
|
*/
|
|
33
34
|
export const STANDALONE_MANIFEST = "dsm-package.yaml";
|
|
34
35
|
export const KNOWN_FIELDS = new Set([
|
|
35
|
-
"
|
|
36
|
-
"
|
|
36
|
+
"ctl",
|
|
37
|
+
"schema",
|
|
38
|
+
"package",
|
|
39
|
+
"version",
|
|
40
|
+
"build",
|
|
41
|
+
"displayname",
|
|
42
|
+
"description",
|
|
43
|
+
"metadata",
|
|
44
|
+
"maintainer",
|
|
45
|
+
"os_min_ver",
|
|
46
|
+
"arches",
|
|
47
|
+
"backend",
|
|
48
|
+
"ui",
|
|
49
|
+
"capabilities",
|
|
50
|
+
"i18n",
|
|
51
|
+
"daemon",
|
|
52
|
+
"identity_gateway",
|
|
53
|
+
"compose",
|
|
54
|
+
"install",
|
|
55
|
+
"uninstall",
|
|
56
|
+
"entry",
|
|
57
|
+
"portal",
|
|
58
|
+
"icon",
|
|
59
|
+
"reproducible",
|
|
60
|
+
"root",
|
|
61
|
+
"login_style",
|
|
37
62
|
]);
|
|
38
63
|
const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
|
|
39
64
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
@@ -66,7 +91,9 @@ class Collector {
|
|
|
66
91
|
return resolved;
|
|
67
92
|
}
|
|
68
93
|
text(value, field) {
|
|
69
|
-
if (typeof value !== "string" ||
|
|
94
|
+
if (typeof value !== "string" ||
|
|
95
|
+
value.trim() === "" ||
|
|
96
|
+
/[\r\n"\\]/.test(value)) {
|
|
70
97
|
return this.fail("STANDALONE_TEXT_INVALID", `${field} must be single-line, non-empty text without " or \\.`);
|
|
71
98
|
}
|
|
72
99
|
return value;
|
|
@@ -75,24 +102,38 @@ class Collector {
|
|
|
75
102
|
export function loadStandaloneManifest(root) {
|
|
76
103
|
const file = join(root, STANDALONE_MANIFEST);
|
|
77
104
|
if (!existsSync(file)) {
|
|
78
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
errors: [
|
|
108
|
+
{
|
|
109
|
+
code: "STANDALONE_MANIFEST_MISSING",
|
|
110
|
+
message: `${STANDALONE_MANIFEST} was not found.`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
79
114
|
}
|
|
80
115
|
let raw;
|
|
81
116
|
try {
|
|
82
117
|
raw = parse(readFileSync(file, "utf8"));
|
|
83
118
|
}
|
|
84
119
|
catch (error) {
|
|
85
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
errors: [{ code: "STANDALONE_MANIFEST_YAML", message: String(error) }],
|
|
123
|
+
};
|
|
86
124
|
}
|
|
87
125
|
const c = new Collector(root);
|
|
88
|
-
if (existsSync(join(root, "compose.yaml")) &&
|
|
126
|
+
if (existsSync(join(root, "compose.yaml")) &&
|
|
127
|
+
!(isRecord(raw) && isRecord(raw.compose)))
|
|
89
128
|
c.fail("PROJECT_RUNTIME_EXCLUSIVE", "compose.yaml and dsm-package.yaml are mutually exclusive; keep only one.");
|
|
90
129
|
if (!isRecord(raw)) {
|
|
91
130
|
c.fail("STANDALONE_MANIFEST_YAML", `${STANDALONE_MANIFEST} must be a YAML object.`);
|
|
92
131
|
return { ok: false, errors: c.errors };
|
|
93
132
|
}
|
|
94
133
|
const manifest = readManifest(raw, c);
|
|
95
|
-
return manifest && c.errors.length === 0
|
|
134
|
+
return manifest && c.errors.length === 0
|
|
135
|
+
? { ok: true, manifest }
|
|
136
|
+
: { ok: false, errors: c.errors };
|
|
96
137
|
}
|
|
97
138
|
function readManifest(raw, c) {
|
|
98
139
|
for (const key of Object.keys(raw)) {
|
|
@@ -112,21 +153,31 @@ function readManifest(raw, c) {
|
|
|
112
153
|
c.fail("STANDALONE_SCHEMA_INVALID", "schema: 2 is required; use schema: 2 with backend.kind: go|binary, and change privilege: root, background and fileStation to capabilities.rootOps: true, capabilities.jobs: true and capabilities.files: session respectively.");
|
|
113
154
|
}
|
|
114
155
|
const capabilities = readCapabilities(raw.capabilities, c);
|
|
156
|
+
if (capabilities.composeMounts && raw.compose === undefined)
|
|
157
|
+
c.fail("CAPABILITY_UNSUPPORTED", "composeMounts applies only to Compose.");
|
|
158
|
+
if (raw.compose !== undefined && raw.backend !== undefined)
|
|
159
|
+
c.fail("PROJECT_RUNTIME_EXCLUSIVE", "Compose uses the SDK gateway, not a dummy App backend; remove backend.");
|
|
115
160
|
const rootOps = readRootOps(capabilities.rootOps === true, c);
|
|
116
|
-
const rootPrograms = raw.root === undefined
|
|
161
|
+
const rootPrograms = raw.root === undefined
|
|
162
|
+
? undefined
|
|
163
|
+
: readRootPrograms(raw.root, capabilities.rootOps === true, rootOps, c);
|
|
117
164
|
const packageId = typeof raw.package === "string" && PACKAGE_ID.test(raw.package)
|
|
118
165
|
? raw.package
|
|
119
166
|
: c.fail("STANDALONE_PACKAGE_INVALID", "package must start with a letter and contain only alphanumerics (2-64 characters).");
|
|
120
167
|
const version = typeof raw.version === "string" && VERSION.test(raw.version)
|
|
121
168
|
? raw.version
|
|
122
169
|
: c.fail("STANDALONE_VERSION_INVALID", "version must be a numeric version such as 1.2.0 (build goes in build).");
|
|
123
|
-
const build = Number.isInteger(raw.build) &&
|
|
170
|
+
const build = Number.isInteger(raw.build) &&
|
|
171
|
+
raw.build >= 1 &&
|
|
172
|
+
raw.build <= 9999
|
|
124
173
|
? raw.build
|
|
125
174
|
: c.fail("STANDALONE_BUILD_INVALID", "build must be an integer from 1 to 9999.");
|
|
126
175
|
const osMinVer = typeof raw.os_min_ver === "string" && OS_MIN_VER.test(raw.os_min_ver)
|
|
127
176
|
? raw.os_min_ver
|
|
128
177
|
: c.fail("STANDALONE_OS_MIN_VER_INVALID", "os_min_ver format is <major>.<minor>-<build>, for example 7.2-64570.");
|
|
129
|
-
if (capabilities.rootOps === true &&
|
|
178
|
+
if (capabilities.rootOps === true &&
|
|
179
|
+
osMinVer &&
|
|
180
|
+
!isDsmAtLeast(osMinVer, DEFAULT_MIN_DSM)) {
|
|
130
181
|
c.fail("STANDALONE_OS_MIN_VER_INVALID", `root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
|
|
131
182
|
}
|
|
132
183
|
const displayname = c.text(raw.displayname, "displayname");
|
|
@@ -141,16 +192,25 @@ function readManifest(raw, c) {
|
|
|
141
192
|
c.fail("STANDALONE_TEXT_INVALID", "The metadata fields are malformed; check the text, email, URLs and screenshots.");
|
|
142
193
|
}
|
|
143
194
|
const arches = readArches(raw.arches, c);
|
|
144
|
-
const binaryProblem = rootOps && arches && (raw.root === undefined || rootPrograms)
|
|
195
|
+
const binaryProblem = rootOps && arches && (raw.root === undefined || rootPrograms)
|
|
196
|
+
? rootBinaryProblem(rootOps, rootPrograms, arches)
|
|
197
|
+
: undefined;
|
|
145
198
|
if (binaryProblem)
|
|
146
199
|
c.fail("STANDALONE_ROOT_PROGRAMS_INVALID", binaryProblem);
|
|
147
|
-
const backend =
|
|
200
|
+
const backend = raw.compose !== undefined && raw.backend === undefined
|
|
201
|
+
? { kind: "binary", files: {} }
|
|
202
|
+
: arches && readBackend(raw.backend, arches, c);
|
|
148
203
|
if (backend && backend.kind !== "binary" && rootOps) {
|
|
149
204
|
// root.run may run a program without settings (its defaults) or with run.
|
|
150
205
|
// The SDK's spec/protocol/v1 Timeouts say how long the executor and its client wait.
|
|
151
206
|
const timeouts = appSdkTimeouts(backend);
|
|
152
|
-
const runnable = rootOps
|
|
153
|
-
.map((
|
|
207
|
+
const runnable = rootOps
|
|
208
|
+
.map((op) => ({ op, setting: rootPrograms?.programs?.[op.name] }))
|
|
209
|
+
.filter(({ setting }) => !setting || setting.run)
|
|
210
|
+
.map(({ op, setting }) => ({
|
|
211
|
+
name: op.name,
|
|
212
|
+
timeoutSeconds: setting?.run?.timeout_s ?? timeouts.rootRunDefaultSeconds,
|
|
213
|
+
}));
|
|
154
214
|
const problem = rootRunDeadlineProblem(runnable, requestTimeoutSeconds(backend), timeouts.rootRunRequestMarginSeconds);
|
|
155
215
|
if (problem)
|
|
156
216
|
c.fail("STANDALONE_REQUEST_TIMEOUT_SHORT", problem);
|
|
@@ -171,14 +231,52 @@ function readManifest(raw, c) {
|
|
|
171
231
|
c.fail("STANDALONE_API_PERMISSIONS_INVALID", `backend/${API_PERMISSIONS_FILE[backend.kind]} must define the centralized API policy: ${String(error)}`);
|
|
172
232
|
}
|
|
173
233
|
}
|
|
174
|
-
const daemon = arches &&
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
234
|
+
const daemon = arches &&
|
|
235
|
+
readDaemon(raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
|
|
236
|
+
const gatewayRaw = isRecord(raw.identity_gateway) ? raw.identity_gateway : {};
|
|
237
|
+
const compose = arches &&
|
|
238
|
+
readComposeApp(raw.compose, c.root, arches, (e) => c.fail(e.code, e.message), {
|
|
239
|
+
package: packageId,
|
|
240
|
+
sdk: typeof gatewayRaw.sdk === "string" ? gatewayRaw.sdk : undefined,
|
|
241
|
+
mode: typeof gatewayRaw.mode === "string" ? gatewayRaw.mode : "none",
|
|
242
|
+
composeMounts: capabilities.composeMounts,
|
|
243
|
+
});
|
|
244
|
+
if (raw.compose !== undefined &&
|
|
245
|
+
(raw.daemon !== undefined ||
|
|
246
|
+
Object.keys(capabilities).some((k) => k !== "composeMounts") ||
|
|
247
|
+
!osMinVer ||
|
|
248
|
+
!isDsmAtLeast(osMinVer, "7.2.1-69057")))
|
|
249
|
+
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose requires DSM >= 7.2.1, no daemon and only the explicit composeMounts capability.");
|
|
250
|
+
const gatewayValue = compose?.web
|
|
251
|
+
? {
|
|
252
|
+
sdk: compose.sdk,
|
|
253
|
+
mode: "none",
|
|
254
|
+
listen: { http: 28000 },
|
|
255
|
+
...gatewayRaw,
|
|
256
|
+
upstream: compose.plan.ingress?.port ??
|
|
257
|
+
compose.web.host_port ??
|
|
258
|
+
compose.web.port,
|
|
259
|
+
}
|
|
260
|
+
: raw.identity_gateway;
|
|
261
|
+
if (compose?.web && gatewayRaw.upstream !== undefined)
|
|
262
|
+
c.fail("COMPOSE_WEB_INVALID", "Compose upstream is derived from the SDK Compose ingress endpoint; remove identity_gateway.upstream.");
|
|
263
|
+
const identityGateway = readIdentityGateway(gatewayValue, daemon || (compose ? { targets: compose.targets } : undefined), (e) => c.fail(e.code, e.message), false, Boolean(compose?.plan.ingress));
|
|
264
|
+
if (compose && identityGateway && compose.sdk !== identityGateway.sdk)
|
|
265
|
+
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose and identity_gateway must use the same SDK release.");
|
|
266
|
+
if (compose &&
|
|
267
|
+
identityGateway?.access === "public" &&
|
|
268
|
+
(raw.entry !== undefined || compose.plan.mounts_blocked))
|
|
269
|
+
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "public ingress cannot bypass entry controls or expose blocked Shared Folder settings.");
|
|
270
|
+
if (compose &&
|
|
271
|
+
identityGateway &&
|
|
272
|
+
(!Object.values(compose.services).some((s) => s.port === identityGateway.upstream) ||
|
|
273
|
+
(!compose.plan.ingress &&
|
|
274
|
+
Object.values(compose.services).some((s) => Object.values(identityGateway.listen).includes(s.port) ||
|
|
275
|
+
s.port === identityGateway.oidc?.issuer_port))))
|
|
180
276
|
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose service ports must contain upstream and not overlap gateway or issuer ports.");
|
|
181
|
-
if (capabilities.jobs &&
|
|
277
|
+
if (capabilities.jobs &&
|
|
278
|
+
raw.daemon !== undefined &&
|
|
279
|
+
capabilities.delegate?.lifetime !== "until-revoked") {
|
|
182
280
|
c.fail("CAPABILITY_COMBINATION_INVALID", "daemon can only coexist with an explicit until-revoked delegate fixed worker; plain capabilities.jobs stays mutually exclusive.");
|
|
183
281
|
}
|
|
184
282
|
if (backend?.kind === "binary") {
|
|
@@ -190,7 +288,10 @@ function readManifest(raw, c) {
|
|
|
190
288
|
if (backend && isBridgeBackend(backend)) {
|
|
191
289
|
// The runtime offers delegate only as the until-revoked files.moveFromStaging integration (spec/bridge/v1).
|
|
192
290
|
const delegate = capabilities.delegate;
|
|
193
|
-
if (delegate &&
|
|
291
|
+
if (delegate &&
|
|
292
|
+
!(delegate.lifetime === "until-revoked" &&
|
|
293
|
+
delegate.operations.join() === "files.moveFromStaging" &&
|
|
294
|
+
delegate.destinations?.length)) {
|
|
194
295
|
c.fail("CAPABILITY_UNSUPPORTED", `For backend.runtime: ${backend.kind}, capabilities.delegate supports only operations: [files.moveFromStaging], destinations and lifetime: until-revoked.`);
|
|
195
296
|
}
|
|
196
297
|
if (raw.daemon !== undefined)
|
|
@@ -198,8 +299,15 @@ function readManifest(raw, c) {
|
|
|
198
299
|
if (permissions)
|
|
199
300
|
checkBridgeMethods(backend, permissions, c);
|
|
200
301
|
}
|
|
201
|
-
const sharedFolder = raw.install === undefined
|
|
202
|
-
|
|
302
|
+
const sharedFolder = raw.install === undefined
|
|
303
|
+
? undefined
|
|
304
|
+
: readSharedFolder(raw.install, (e) => c.fail(e.code, e.message));
|
|
305
|
+
const entry = raw.entry === undefined
|
|
306
|
+
? undefined
|
|
307
|
+
: raw.compose !== undefined &&
|
|
308
|
+
(raw.entry === "admin-only" || raw.entry === "app-privilege")
|
|
309
|
+
? raw.entry
|
|
310
|
+
: readEntry(raw.entry, backend, permissions?.policy, c);
|
|
203
311
|
if (entry === "admin-only" && permissions) {
|
|
204
312
|
const floored = floorAtAdministrators(permissions.policy, permissions.sdk);
|
|
205
313
|
if (floored.widened.length) {
|
|
@@ -207,8 +315,26 @@ function readManifest(raw, c) {
|
|
|
207
315
|
}
|
|
208
316
|
permissions.policy = floored.policy;
|
|
209
317
|
}
|
|
210
|
-
const ui =
|
|
211
|
-
|
|
318
|
+
const ui = raw.compose !== undefined
|
|
319
|
+
? {
|
|
320
|
+
dir: c.root,
|
|
321
|
+
...(isRecord(raw.ui) && raw.ui.launch !== undefined
|
|
322
|
+
? { launch: raw.ui.launch }
|
|
323
|
+
: {}),
|
|
324
|
+
}
|
|
325
|
+
: readUi(raw.ui, c);
|
|
326
|
+
if (raw.compose !== undefined &&
|
|
327
|
+
isRecord(raw.ui) &&
|
|
328
|
+
Object.keys(raw.ui).some((k) => k !== "launch"))
|
|
329
|
+
c.fail("STANDALONE_UI_INVALID", "Compose uses the SDK gateway; ui accepts only launch.");
|
|
330
|
+
if (raw.compose !== undefined &&
|
|
331
|
+
ui?.launch !== undefined &&
|
|
332
|
+
!isWebLaunchMode(ui.launch))
|
|
333
|
+
c.fail("STANDALONE_UI_LAUNCH_INVALID", "ui.launch must be dsm-window or new-page.");
|
|
334
|
+
if (backend &&
|
|
335
|
+
isBridgeBackend(backend) &&
|
|
336
|
+
ui &&
|
|
337
|
+
overlaps(backend.dir, ui.dir)) {
|
|
212
338
|
c.fail("STANDALONE_BRIDGE_APP_IN_UI", "backend.dir and ui.dir must not overlap: DSM serves the files under ui/ directly, so the App directory (target/app) cannot be inside it.");
|
|
213
339
|
}
|
|
214
340
|
if (capabilities.identity && ui && !ui.build)
|
|
@@ -216,40 +342,72 @@ function readManifest(raw, c) {
|
|
|
216
342
|
if (capabilities.delegate?.scope === "install-selected" && ui && !ui.build)
|
|
217
343
|
c.fail("CAPABILITY_COMBINATION_INVALID", "The delegate install-selected scope can only be confirmed in a Vue panel; plain UI does not support it, use a vue UI (init --ui vue).");
|
|
218
344
|
if (backend && backend.kind !== "binary") {
|
|
219
|
-
for (const problem of sdkProblems(backend, ui?.build?.cwd, isBridgeBackend(backend) ||
|
|
345
|
+
for (const problem of sdkProblems(backend, ui?.build?.cwd, isBridgeBackend(backend) ||
|
|
346
|
+
Boolean(capabilities.delegate || capabilities.rootOps || capabilities.webapi)))
|
|
220
347
|
c.fail(problem.code, problem.message);
|
|
221
348
|
}
|
|
222
349
|
const i18n = raw.i18n === undefined ? undefined : readI18n(raw.i18n, c);
|
|
223
350
|
const icon = raw.icon === undefined ? undefined : readIcon(raw.icon, c);
|
|
224
351
|
if (capabilities.webapi && backend) {
|
|
225
352
|
const sdk = sdkVersionOrUndefined(backend);
|
|
226
|
-
if (!sdk ||
|
|
353
|
+
if (!sdk ||
|
|
354
|
+
(Number(sdk.split(".")[0]) === 0 && Number(sdk.split(".")[1]) < 11))
|
|
227
355
|
c.fail("SDK_UNSUPPORTED", "capabilities.webapi requires SDK 0.11.0 or later; update backend and frontend together.");
|
|
228
|
-
const raw = Object.values(capabilities.webapi).some(profile => (profile.raw_max_bytes ?? 0) > 0);
|
|
229
|
-
const [major, minor, patch] = (sdk ??
|
|
230
|
-
if (raw &&
|
|
356
|
+
const raw = Object.values(capabilities.webapi).some((profile) => (profile.raw_max_bytes ?? 0) > 0);
|
|
357
|
+
const [major, minor, patch] = (sdk ?? "0.0.0").split(".").map(Number);
|
|
358
|
+
if (raw &&
|
|
359
|
+
(!sdk || (major === 0 && (minor < 12 || (minor === 12 && patch < 1)))))
|
|
231
360
|
c.fail("SDK_UNSUPPORTED", "Raw WebAPI profiles require SDK 0.12.1 or later; update backend and frontend together.");
|
|
232
361
|
}
|
|
233
|
-
const uninstall = readUninstall(raw.uninstall, {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
362
|
+
const uninstall = readUninstall(raw.uninstall, {
|
|
363
|
+
rootOps: capabilities.rootOps === true,
|
|
364
|
+
ops: rootOps,
|
|
365
|
+
en: i18n?.catalogs?.en,
|
|
366
|
+
}, (e) => c.fail(e.code, e.message));
|
|
367
|
+
const appScripts = readAppScripts(c.root, (e) => c.fail(e.code, e.message));
|
|
368
|
+
const portal = raw.portal === undefined || !packageId
|
|
369
|
+
? undefined
|
|
370
|
+
: (readPortalDeclaration(raw.portal, packageId) ??
|
|
371
|
+
c.fail("STANDALONE_PORTAL_INVALID", "portal must be {} or { alias: <1-64 lowercase alphanumerics or -, not a DSM reserved path> }."));
|
|
372
|
+
if (portal &&
|
|
373
|
+
!capabilities.rootOps &&
|
|
374
|
+
!capabilities.delegate &&
|
|
375
|
+
!capabilities.webapi) {
|
|
238
376
|
c.fail("STANDALONE_PORTAL_REQUIRES_EXECUTOR", PORTAL_REQUIRES_EXECUTOR);
|
|
239
377
|
}
|
|
240
378
|
if (portal && osMinVer && !isDsmAtLeast(osMinVer, PORTAL_EXECUTOR_MIN_DSM)) {
|
|
241
379
|
c.fail("STANDALONE_PORTAL_MIN_DSM", portalMinDsmMessage(osMinVer, "portal"));
|
|
242
380
|
}
|
|
243
381
|
const entryProblem = portal && packageId && displayname && osMinVer && portalByExecutor(osMinVer)
|
|
244
|
-
? portalEntryProblem(packageId, {
|
|
382
|
+
? portalEntryProblem(packageId, {
|
|
383
|
+
application: displayname,
|
|
384
|
+
alias: portal.alias,
|
|
385
|
+
})
|
|
386
|
+
: undefined;
|
|
245
387
|
if (entryProblem)
|
|
246
388
|
c.fail("STANDALONE_PORTAL_ENTRY_INVALID", entryProblem);
|
|
247
|
-
const loginStyle = raw.login_style === undefined
|
|
248
|
-
|
|
249
|
-
|
|
389
|
+
const loginStyle = raw.login_style === undefined
|
|
390
|
+
? undefined
|
|
391
|
+
: readLoginStyle(raw.login_style, raw.portal !== undefined, c);
|
|
392
|
+
if (!packageId ||
|
|
393
|
+
!version ||
|
|
394
|
+
!build ||
|
|
395
|
+
!osMinVer ||
|
|
396
|
+
!displayname ||
|
|
397
|
+
!description ||
|
|
398
|
+
!maintainer ||
|
|
399
|
+
!arches ||
|
|
400
|
+
!backend ||
|
|
401
|
+
!ui ||
|
|
402
|
+
(capabilities.rootOps === true && !rootOps)) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
if (raw.ctl !== undefined && (!isRecord(raw.ctl) || typeof raw.ctl.enabled !== "boolean" || raw.ctl.enabled && (!raw.ctl.commands || typeof raw.ctl.commands !== "object"))) {
|
|
406
|
+
c.fail("CTL_DECLARATION_INVALID", "ctl requires enabled and explicit commands.");
|
|
250
407
|
return undefined;
|
|
251
408
|
}
|
|
252
409
|
return {
|
|
410
|
+
...(raw.ctl ? { ctl: raw.ctl } : {}),
|
|
253
411
|
root: c.root,
|
|
254
412
|
package: packageId,
|
|
255
413
|
version,
|
|
@@ -262,7 +420,13 @@ function readManifest(raw, c) {
|
|
|
262
420
|
os_min_ver: osMinVer,
|
|
263
421
|
arches,
|
|
264
422
|
backend,
|
|
265
|
-
...(permissions
|
|
423
|
+
...(permissions
|
|
424
|
+
? {
|
|
425
|
+
apiPermissions: permissions.policy,
|
|
426
|
+
apiPermissionsDigest: permissions.digest,
|
|
427
|
+
apiSDKPermissionsDigest: permissions.sdkDigest,
|
|
428
|
+
}
|
|
429
|
+
: {}),
|
|
266
430
|
ui,
|
|
267
431
|
...(i18n ? { i18n } : {}),
|
|
268
432
|
capabilities,
|
|
@@ -290,14 +454,14 @@ function checkBridgeMethods(backend, permissions, c) {
|
|
|
290
454
|
* Without methods only the SDK names of the policy can be checked.
|
|
291
455
|
*/
|
|
292
456
|
export function bridgeMethodProblems(methods, permissions, file) {
|
|
293
|
-
const sdk = new Set(Object.values(permissions.sdk).flatMap(names => Object.keys(names)));
|
|
457
|
+
const sdk = new Set(Object.values(permissions.sdk).flatMap((names) => Object.keys(names)));
|
|
294
458
|
const problems = [];
|
|
295
|
-
const taken = (methods ?? []).filter(method => sdk.has(method));
|
|
459
|
+
const taken = (methods ?? []).filter((method) => sdk.has(method));
|
|
296
460
|
if (taken.length)
|
|
297
461
|
problems.push(`App methods must not use an SDK method name: ${taken.join(", ")}.`);
|
|
298
462
|
if (!methods)
|
|
299
463
|
return problems;
|
|
300
|
-
const unlisted = Object.keys(permissions.policy.methods).filter(method => !sdk.has(method) && !methods.includes(method));
|
|
464
|
+
const unlisted = Object.keys(permissions.policy.methods).filter((method) => !sdk.has(method) && !methods.includes(method));
|
|
301
465
|
if (unlisted.length)
|
|
302
466
|
problems.push(`${file} sets methods the App does not have: ${unlisted.join(", ")}; the runtime refuses to start.`);
|
|
303
467
|
return problems;
|
|
@@ -318,7 +482,12 @@ function readIcon(value, c) {
|
|
|
318
482
|
return c.fail("STANDALONE_ICON_INVALID", `icon: ${error.message}`);
|
|
319
483
|
}
|
|
320
484
|
}
|
|
321
|
-
const LOGIN_STYLE_FIELDS = [
|
|
485
|
+
const LOGIN_STYLE_FIELDS = [
|
|
486
|
+
"color",
|
|
487
|
+
"wallpaper",
|
|
488
|
+
"wallpaper_thumbnail",
|
|
489
|
+
"title",
|
|
490
|
+
];
|
|
322
491
|
/** `login_style`: a portal's colour, wallpaper (with its thumbnail) and title image, each optional. */
|
|
323
492
|
function readLoginStyle(value, portal, c) {
|
|
324
493
|
const invalid = (message) => c.fail("STANDALONE_LOGIN_STYLE_INVALID", message);
|
|
@@ -326,13 +495,15 @@ function readLoginStyle(value, portal, c) {
|
|
|
326
495
|
return invalid("login_style must be a mapping with color, wallpaper, wallpaper_thumbnail or title.");
|
|
327
496
|
if (!portal)
|
|
328
497
|
return invalid("login_style styles the Application Portal login page; declare portal as well, or remove login_style.");
|
|
329
|
-
const unknown = Object.keys(value).filter(key => !LOGIN_STYLE_FIELDS.includes(key));
|
|
498
|
+
const unknown = Object.keys(value).filter((key) => !LOGIN_STYLE_FIELDS.includes(key));
|
|
330
499
|
if (unknown.length)
|
|
331
500
|
return invalid(`login_style does not accept ${unknown.join(", ")}; use ${LOGIN_STYLE_FIELDS.join(", ")}.`);
|
|
332
|
-
if (value.color !== undefined &&
|
|
501
|
+
if (value.color !== undefined &&
|
|
502
|
+
(typeof value.color !== "string" || !LOGIN_COLOR.test(value.color))) {
|
|
333
503
|
return invalid('login_style.color must be a hex colour such as "#0F766E".');
|
|
334
504
|
}
|
|
335
|
-
if ((value.wallpaper === undefined) !==
|
|
505
|
+
if ((value.wallpaper === undefined) !==
|
|
506
|
+
(value.wallpaper_thumbnail === undefined)) {
|
|
336
507
|
return invalid("login_style.wallpaper and login_style.wallpaper_thumbnail go together: DSM uses a package wallpaper only when both are set.");
|
|
337
508
|
}
|
|
338
509
|
const image = (field) => {
|
|
@@ -340,18 +511,30 @@ function readLoginStyle(value, portal, c) {
|
|
|
340
511
|
return undefined;
|
|
341
512
|
const path = c.path(value[field], `login_style.${field}`);
|
|
342
513
|
const problem = path && loginImageProblem(field, path);
|
|
343
|
-
return problem
|
|
514
|
+
return problem
|
|
515
|
+
? c.fail("STANDALONE_LOGIN_STYLE_IMAGE_INVALID", `login_style.${field}: ${problem}`)
|
|
516
|
+
: path;
|
|
344
517
|
};
|
|
345
|
-
const [wallpaper, thumbnail, title] = [
|
|
518
|
+
const [wallpaper, thumbnail, title] = [
|
|
519
|
+
image("wallpaper"),
|
|
520
|
+
image("wallpaper_thumbnail"),
|
|
521
|
+
image("title"),
|
|
522
|
+
];
|
|
346
523
|
return {
|
|
347
524
|
...(value.color !== undefined ? { color: value.color } : {}),
|
|
348
|
-
...(wallpaper && thumbnail
|
|
525
|
+
...(wallpaper && thumbnail
|
|
526
|
+
? { wallpaper: { image: wallpaper, thumbnail } }
|
|
527
|
+
: {}),
|
|
349
528
|
...(title ? { title } : {}),
|
|
350
529
|
};
|
|
351
530
|
}
|
|
352
531
|
/** Where each backend kind's CGI main wraps its authenticator in the SDK entry gate. */
|
|
353
532
|
const ENTRY_GATE = {
|
|
354
|
-
go: {
|
|
533
|
+
go: {
|
|
534
|
+
main: (b) => (b.kind === "go" ? join(b.dir, b.main, "main.go") : ""),
|
|
535
|
+
call: "dsm.NewEntryGate(",
|
|
536
|
+
sdk: `backend/go.mod must require ${SDK_MODULE}`,
|
|
537
|
+
},
|
|
355
538
|
};
|
|
356
539
|
/** The SDK version, or none when the backend has none or pins it wrongly (reported by sdkProblems). */
|
|
357
540
|
function sdkVersionOrUndefined(backend) {
|
|
@@ -376,8 +559,10 @@ function readEntry(value, backend, policy, c) {
|
|
|
376
559
|
// A bridge App's entry gate is the runtime's (bridge.json access).
|
|
377
560
|
const gate = isBridgeBackend(backend) ? undefined : ENTRY_GATE[backend.kind];
|
|
378
561
|
const main = gate?.main(backend) ?? "";
|
|
379
|
-
if (!isBridgeBackend(backend) &&
|
|
380
|
-
|
|
562
|
+
if ((!isBridgeBackend(backend) &&
|
|
563
|
+
(!gate || !sdkVersionOrUndefined(backend))) ||
|
|
564
|
+
(gate &&
|
|
565
|
+
(!existsSync(main) || !readFileSync(main, "utf8").includes(gate.call)))) {
|
|
381
566
|
const shown = main ? relative(c.root, main) : "the backend.main main.go";
|
|
382
567
|
return c.fail("STANDALONE_ENTRY_UNSUPPORTED", `entry needs the SDK entry gate: ${gate?.sdk ?? `backend/go.mod must require ${SDK_MODULE}`}, and wrap the authenticator with ${gate?.call.slice(0, -1) ?? "dsm.NewEntryGate"} in ${shown}.`);
|
|
383
568
|
}
|
|
@@ -391,7 +576,9 @@ function readRootPrograms(value, enabled, ops, c) {
|
|
|
391
576
|
if (enabled && !ops)
|
|
392
577
|
return undefined;
|
|
393
578
|
const read = readRootSettings(value, ops);
|
|
394
|
-
return "settings" in read
|
|
579
|
+
return "settings" in read
|
|
580
|
+
? read.settings
|
|
581
|
+
: c.fail("STANDALONE_ROOT_PROGRAMS_INVALID", read.error);
|
|
395
582
|
}
|
|
396
583
|
function readRootOps(enabled, c) {
|
|
397
584
|
const dir = join(c.root, ROOT_OPS_DIR);
|
|
@@ -408,10 +595,10 @@ function readRootOps(enabled, c) {
|
|
|
408
595
|
return loaded.ops;
|
|
409
596
|
}
|
|
410
597
|
function readArches(value, c) {
|
|
411
|
-
const ok = Array.isArray(value)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
598
|
+
const ok = Array.isArray(value) &&
|
|
599
|
+
value.length > 0 &&
|
|
600
|
+
value.every((arch) => typeof arch === "string" && isNativeArchFamily(arch)) &&
|
|
601
|
+
new Set(value).size === value.length;
|
|
415
602
|
return ok
|
|
416
603
|
? value
|
|
417
604
|
: c.fail("STANDALONE_ARCHES_INVALID", "arches must be a list of unique x86_64/aarch64/armv7 values, at least one.");
|
|
@@ -425,7 +612,7 @@ function readBackend(value, arches, c) {
|
|
|
425
612
|
value = { ...go, kind: "go" };
|
|
426
613
|
}
|
|
427
614
|
else if (BRIDGE_RUNTIMES.includes(value.runtime)) {
|
|
428
|
-
return readBridgeBackend(value, dir => c.path(dir, "backend.dir"), (code, message) => c.fail(code, message));
|
|
615
|
+
return readBridgeBackend(value, (dir) => c.path(dir, "backend.dir"), (code, message) => c.fail(code, message));
|
|
429
616
|
}
|
|
430
617
|
else {
|
|
431
618
|
return c.fail("STANDALONE_BACKEND_INVALID", "backend.runtime must be go, python, node or rust.");
|
|
@@ -435,7 +622,11 @@ function readBackend(value, arches, c) {
|
|
|
435
622
|
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind must be go or binary (or name go, python, node, rust with backend.runtime).");
|
|
436
623
|
}
|
|
437
624
|
if (value.kind === "go") {
|
|
438
|
-
if (Object.keys(value).some(key => !["kind", "dir", "main", "timeout_seconds"].includes(key)) ||
|
|
625
|
+
if (Object.keys(value).some((key) => !["kind", "dir", "main", "timeout_seconds"].includes(key)) ||
|
|
626
|
+
(value.main !== undefined &&
|
|
627
|
+
(typeof value.main !== "string" ||
|
|
628
|
+
!GO_MAIN.test(value.main) ||
|
|
629
|
+
value.main.includes("..")))) {
|
|
439
630
|
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: go needs dir, with optional main (./cmd/app by default) and timeout_seconds.");
|
|
440
631
|
}
|
|
441
632
|
const timeout = value.timeout_seconds;
|
|
@@ -444,16 +635,23 @@ function readBackend(value, arches, c) {
|
|
|
444
635
|
const dir = c.path(value.dir, "backend.dir");
|
|
445
636
|
if (!dir)
|
|
446
637
|
return undefined;
|
|
447
|
-
const backend = {
|
|
638
|
+
const backend = {
|
|
639
|
+
kind: "go",
|
|
640
|
+
dir,
|
|
641
|
+
main: value.main ?? "./cmd/app",
|
|
642
|
+
};
|
|
448
643
|
if (timeout === undefined)
|
|
449
644
|
return backend;
|
|
450
|
-
return goReadsTimeout(backend, c)
|
|
645
|
+
return goReadsTimeout(backend, c)
|
|
646
|
+
? { ...backend, timeoutSeconds: timeout }
|
|
647
|
+
: undefined;
|
|
451
648
|
}
|
|
452
649
|
if (value.timeout_seconds !== undefined) {
|
|
453
650
|
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: binary does not take timeout_seconds: a prebuilt CGI sets its own request deadline.");
|
|
454
651
|
}
|
|
455
652
|
const binary = value.files;
|
|
456
|
-
if (!isRecord(binary) ||
|
|
653
|
+
if (!isRecord(binary) ||
|
|
654
|
+
Object.keys(value).some((key) => !["kind", "files"].includes(key))) {
|
|
457
655
|
return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: binary needs files keyed by architecture pointing at CGI executables.");
|
|
458
656
|
}
|
|
459
657
|
const files = {};
|
|
@@ -469,7 +667,9 @@ function readBackend(value, arches, c) {
|
|
|
469
667
|
c.fail("STANDALONE_BACKEND_INVALID", `backend.files.${key} is not in arches.`);
|
|
470
668
|
}
|
|
471
669
|
}
|
|
472
|
-
return Object.values(files).every(Boolean)
|
|
670
|
+
return Object.values(files).every(Boolean)
|
|
671
|
+
? { kind: "binary", files }
|
|
672
|
+
: undefined;
|
|
473
673
|
}
|
|
474
674
|
/**
|
|
475
675
|
* The build passes the deadline as -X main.requestTimeout; a main without
|
|
@@ -487,25 +687,38 @@ function readUi(value, c) {
|
|
|
487
687
|
if (!isRecord(value)) {
|
|
488
688
|
return c.fail("STANDALONE_UI_INVALID", "ui must contain dir.");
|
|
489
689
|
}
|
|
690
|
+
if (value.launch !== undefined && !isWebLaunchMode(value.launch))
|
|
691
|
+
return c.fail("STANDALONE_UI_LAUNCH_INVALID", "ui.launch must be dsm-window or new-page. Application Portal is a separate opt-in.");
|
|
692
|
+
const launch = value.launch === undefined ? {} : { launch: value.launch };
|
|
490
693
|
const dir = c.path(value.dir, "ui.dir");
|
|
491
694
|
const read = value.window === undefined ? undefined : readWindowSize(value.window);
|
|
492
695
|
if (read && "error" in read)
|
|
493
696
|
return c.fail("STANDALONE_UI_WINDOW_INVALID", read.error);
|
|
494
697
|
const window = read ? { window: read.size } : {};
|
|
495
698
|
if (value.build === undefined) {
|
|
496
|
-
return dir ? { dir, ...window } : undefined;
|
|
699
|
+
return dir ? { dir, ...launch, ...window } : undefined;
|
|
497
700
|
}
|
|
498
701
|
const build = value.build;
|
|
499
702
|
const command = isRecord(build) ? build.command : undefined;
|
|
500
|
-
if (!Array.isArray(command) ||
|
|
703
|
+
if (!Array.isArray(command) ||
|
|
704
|
+
command.length === 0 ||
|
|
705
|
+
!command.every((arg) => typeof arg === "string" && arg !== "")) {
|
|
501
706
|
return c.fail("STANDALONE_UI_BUILD_INVALID", "ui.build.command must be an argv string array (not going through a shell), for example [npm, run, build].");
|
|
502
707
|
}
|
|
503
708
|
const cwd = c.path(build.cwd, "ui.build.cwd");
|
|
504
|
-
return dir && cwd
|
|
709
|
+
return dir && cwd
|
|
710
|
+
? {
|
|
711
|
+
dir,
|
|
712
|
+
build: { cwd, command: command },
|
|
713
|
+
...launch,
|
|
714
|
+
...window,
|
|
715
|
+
}
|
|
716
|
+
: undefined;
|
|
505
717
|
}
|
|
506
718
|
function readI18n(value, c) {
|
|
507
719
|
const invalid = (why) => c.fail("STANDALONE_I18N_INVALID", `i18n ${why}`);
|
|
508
|
-
if (!isRecord(value) ||
|
|
720
|
+
if (!isRecord(value) ||
|
|
721
|
+
Object.keys(value).some((key) => !I18N_FIELDS.has(key))) {
|
|
509
722
|
return invalid("Only messages (the message directory), glossary (the terminology table) and info (per-language INFO text) are accepted.");
|
|
510
723
|
}
|
|
511
724
|
const info = readInfoTranslations(value.info, {
|
|
@@ -513,10 +726,14 @@ function readI18n(value, c) {
|
|
|
513
726
|
text: (text, field) => c.text(text, field),
|
|
514
727
|
});
|
|
515
728
|
if (value.messages === undefined) {
|
|
516
|
-
return value.glossary === undefined
|
|
729
|
+
return value.glossary === undefined
|
|
730
|
+
? info && { info }
|
|
731
|
+
: invalid("glossary needs messages.");
|
|
517
732
|
}
|
|
518
733
|
const dir = c.path(value.messages, "i18n.messages");
|
|
519
|
-
const glossary = value.glossary === undefined
|
|
734
|
+
const glossary = value.glossary === undefined
|
|
735
|
+
? undefined
|
|
736
|
+
: c.path(value.glossary, "i18n.glossary");
|
|
520
737
|
if (!dir || (value.glossary !== undefined && !glossary)) {
|
|
521
738
|
return undefined;
|
|
522
739
|
}
|
|
@@ -527,7 +744,9 @@ function readI18n(value, c) {
|
|
|
527
744
|
}
|
|
528
745
|
const drift = glossary ? checkGlossary(glossary, loaded.catalogs) : [];
|
|
529
746
|
drift.forEach((e) => c.fail(e.code, e.message));
|
|
530
|
-
return info && drift.length === 0
|
|
747
|
+
return info && drift.length === 0
|
|
748
|
+
? { catalogs: loaded.catalogs, info }
|
|
749
|
+
: undefined;
|
|
531
750
|
}
|
|
532
751
|
/**
|
|
533
752
|
* The nearest directory at or above `start` that holds dsm-package.yaml,
|
|
@@ -558,7 +777,9 @@ function readCapabilities(value, c) {
|
|
|
558
777
|
}
|
|
559
778
|
const result = {};
|
|
560
779
|
for (const [name, setting] of Object.entries(value)) {
|
|
561
|
-
if (name === "
|
|
780
|
+
if (name === "composeMounts" && setting === true)
|
|
781
|
+
result.composeMounts = true;
|
|
782
|
+
else if (name === "webapi") {
|
|
562
783
|
if (validWebAPIProfiles(setting))
|
|
563
784
|
result.webapi = setting;
|
|
564
785
|
else
|
|
@@ -577,7 +798,7 @@ function readCapabilities(value, c) {
|
|
|
577
798
|
else if (name === "rootOps" && setting === true)
|
|
578
799
|
result.rootOps = true;
|
|
579
800
|
else if (name === "jobs") {
|
|
580
|
-
result.jobs = readBackground(setting === true ? { worker: "sha256-v1" } : setting, e => c.fail("CAPABILITY_INVALID", e.message.replace("background", "capabilities.jobs")));
|
|
801
|
+
result.jobs = readBackground(setting === true ? { worker: "sha256-v1" } : setting, (e) => c.fail("CAPABILITY_INVALID", e.message.replace("background", "capabilities.jobs")));
|
|
581
802
|
}
|
|
582
803
|
else
|
|
583
804
|
c.fail("CAPABILITY_INVALID", `Unsupported capabilities.${name} value; use files: session, identity: standalone, jobs: true or {worker: sha256-v1}, rootOps: true.`);
|