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