@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.
Files changed (119) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +44 -1
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -850
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/app-launch.d.ts +11 -0
  14. package/dist/contract/app-launch.js +18 -0
  15. package/dist/contract/web-launch.d.ts +3 -13
  16. package/dist/contract/web-launch.js +14 -81
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/error-registry.js +1020 -831
  46. package/dist/info.js +120 -28
  47. package/dist/init.js +188 -64
  48. package/dist/inspect.js +21 -6
  49. package/dist/runtime/executor-digests.json +1 -12
  50. package/dist/skill.js +51 -9
  51. package/dist/standalone/api-description-inspect.d.ts +4 -0
  52. package/dist/standalone/api-description-inspect.js +12 -0
  53. package/dist/standalone/api-description.d.ts +7 -0
  54. package/dist/standalone/api-description.js +73 -0
  55. package/dist/standalone/build.d.ts +3 -0
  56. package/dist/standalone/build.js +140 -33
  57. package/dist/standalone/compose-errors.d.ts +3 -0
  58. package/dist/standalone/compose-errors.js +243 -0
  59. package/dist/standalone/compose.d.ts +97 -10
  60. package/dist/standalone/compose.js +306 -75
  61. package/dist/standalone/daemon.js +111 -29
  62. package/dist/standalone/dsm-window.js +55 -9
  63. package/dist/standalone/identity-gateway.d.ts +9 -2
  64. package/dist/standalone/identity-gateway.js +282 -66
  65. package/dist/standalone/inspect-spk.d.ts +15 -0
  66. package/dist/standalone/inspect-spk.js +714 -128
  67. package/dist/standalone/lifecycle.js +124 -42
  68. package/dist/standalone/manifest.d.ts +10 -1
  69. package/dist/standalone/manifest.js +308 -87
  70. package/dist/standalone/resource.js +6 -2
  71. package/dist/standalone/spk.d.ts +4 -1
  72. package/dist/standalone/spk.js +139 -39
  73. package/dist/template-paths.d.ts +0 -2
  74. package/dist/template-paths.js +0 -5
  75. package/dist/test-command.d.ts +1 -1
  76. package/dist/test-command.js +1 -1
  77. package/dist/validate.d.ts +3 -3
  78. package/dist/validate.js +32 -140
  79. package/package.json +5 -3
  80. package/schema/synology-app.schema.json +1 -1
  81. package/schema/synopkgland-api-v1.schema.json +423 -0
  82. package/skills/SKILL.md +34 -1
  83. package/skills/reference/compose.md +349 -0
  84. package/skills/reference/create.md +26 -4
  85. package/skills/reference/ctl.md +41 -0
  86. package/skills/reference/deploy.md +1 -1
  87. package/skills/reference/files.md +4 -0
  88. package/skills/reference/identity-gateway.md +8 -138
  89. package/skills/reference/portal.md +31 -0
  90. package/skills/reference/ui-vue.md +31 -0
  91. package/skills/reference/wrap.md +10 -30
  92. package/assets/go/daemon/fixture_linux_test.go +0 -15
  93. package/assets/go/daemon/fixture_other_test.go +0 -7
  94. package/assets/go/daemon/go.mod +0 -5
  95. package/assets/go/daemon/main.go +0 -636
  96. package/assets/go/daemon/main_test.go +0 -315
  97. package/assets/go/daemon/network.go +0 -107
  98. package/assets/go/daemon/network_test.go +0 -86
  99. package/assets/go/daemon/process_linux.go +0 -55
  100. package/assets/go/daemon/process_linux_test.go +0 -161
  101. package/assets/go/daemon/process_other.go +0 -14
  102. package/assets/go/daemon/testdata/child.go +0 -44
  103. package/assets/go/gateway/auth.go +0 -101
  104. package/assets/go/gateway/auth_test.go +0 -216
  105. package/assets/go/gateway/cgi.go +0 -133
  106. package/assets/go/gateway/cgi_test.go +0 -156
  107. package/assets/go/gateway/csrf.go +0 -73
  108. package/assets/go/gateway/csrf_test.go +0 -155
  109. package/assets/go/gateway/directory.go +0 -108
  110. package/assets/go/gateway/directory_test.go +0 -202
  111. package/assets/go/gateway/gateway.go +0 -412
  112. package/assets/go/gateway/gateway_test.go +0 -676
  113. package/assets/go/gateway/go.mod +0 -5
  114. package/assets/go/gateway/groups.go +0 -66
  115. package/assets/go/gateway/main.go +0 -45
  116. package/dist/build-spk/gateway.d.ts +0 -31
  117. package/dist/build-spk/gateway.js +0 -60
  118. package/dist/compose/privilege.d.ts +0 -14
  119. package/dist/compose/privilege.js +0 -102
@@ -1,39 +1,64 @@
1
- import { validWebAPIProfiles } from '../runtime/webapi-profile.js';
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
- "schema", "package", "version", "build", "displayname", "description",
36
- "metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "identity_gateway", "compose", "install", "uninstall", "entry", "portal", "icon", "reproducible", "root", "login_style",
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" || value.trim() === "" || /[\r\n"\\]/.test(value)) {
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 { ok: false, errors: [{ code: "STANDALONE_MANIFEST_MISSING", message: `${STANDALONE_MANIFEST} was not found.` }] };
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 { ok: false, errors: [{ code: "STANDALONE_MANIFEST_YAML", message: String(error) }] };
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")) && !(isRecord(raw) && isRecord(raw.compose) && raw.compose.file === "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 ? { ok: true, manifest } : { ok: false, errors: c.errors };
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 ? undefined : readRootPrograms(raw.root, capabilities.rootOps === true, rootOps, c);
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) && raw.build >= 1 && raw.build <= 9999
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 && osMinVer && !isDsmAtLeast(osMinVer, DEFAULT_MIN_DSM)) {
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) ? rootBinaryProblem(rootOps, rootPrograms, arches) : undefined;
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 = arches && readBackend(raw.backend, arches, c);
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.map(op => ({ op, setting: rootPrograms?.programs?.[op.name] })).filter(({ setting }) => !setting || setting.run)
153
- .map(({ op, setting }) => ({ name: op.name, timeoutSeconds: setting?.run?.timeout_s ?? timeouts.rootRunDefaultSeconds }));
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 && readDaemon(raw.daemon, c.root, arches, e => c.fail(e.code, e.message));
175
- const compose = arches && readComposeApp(raw.compose, c.root, arches, e => c.fail(e.code, e.message));
176
- if (raw.compose !== undefined && (raw.daemon !== undefined || raw.identity_gateway === undefined || Object.keys(capabilities).length || !osMinVer || !isDsmAtLeast(osMinVer, "7.2.1-69057")))
177
- c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose requires identity_gateway, DSM >= 7.2.1, no daemon and no capabilities.");
178
- const identityGateway = readIdentityGateway(raw.identity_gateway, daemon || (compose ? { targets: compose.targets } : undefined), e => c.fail(e.code, e.message));
179
- if (compose && identityGateway && (!Object.values(compose.services).some(s => s.port === identityGateway.upstream) || Object.values(compose.services).some(s => Object.values(identityGateway.listen).includes(s.port) || s.port === identityGateway.oidc?.issuer_port)))
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 && raw.daemon !== undefined && capabilities.delegate?.lifetime !== 'until-revoked') {
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 && !(delegate.lifetime === "until-revoked" && delegate.operations.join() === "files.moveFromStaging" && delegate.destinations?.length)) {
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 ? undefined : readSharedFolder(raw.install, e => c.fail(e.code, e.message));
202
- const entry = raw.entry === undefined ? undefined : readEntry(raw.entry, backend, permissions?.policy, c);
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 = readUi(raw.ui, c);
211
- if (backend && isBridgeBackend(backend) && ui && overlaps(backend.dir, ui.dir)) {
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) || Boolean(capabilities.delegate || capabilities.rootOps || capabilities.webapi)))
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 || Number(sdk.split('.')[0]) === 0 && Number(sdk.split('.')[1]) < 11)
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 ?? '0.0.0').split('.').map(Number);
230
- if (raw && (!sdk || major === 0 && (minor < 12 || minor === 12 && patch < 1)))
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, { rootOps: capabilities.rootOps === true, ops: rootOps, en: i18n?.catalogs?.en }, e => c.fail(e.code, e.message));
234
- const appScripts = readAppScripts(c.root, e => c.fail(e.code, e.message));
235
- const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
236
- ?? c.fail("STANDALONE_PORTAL_INVALID", "portal must be {} or { alias: <1-64 lowercase alphanumerics or -, not a DSM reserved path> }.");
237
- if (portal && !capabilities.rootOps && !capabilities.delegate && !capabilities.webapi) {
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, { application: displayname, alias: portal.alias }) : undefined;
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 ? undefined : readLoginStyle(raw.login_style, raw.portal !== undefined, c);
248
- if (!packageId || !version || !build || !osMinVer || !displayname || !description || !maintainer
249
- || !arches || !backend || !ui || (capabilities.rootOps === true && !rootOps)) {
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 ? { apiPermissions: permissions.policy, apiPermissionsDigest: permissions.digest, apiSDKPermissionsDigest: permissions.sdkDigest } : {}),
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 = ["color", "wallpaper", "wallpaper_thumbnail", "title"];
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 && (typeof value.color !== "string" || !LOGIN_COLOR.test(value.color))) {
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) !== (value.wallpaper_thumbnail === 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 ? c.fail("STANDALONE_LOGIN_STYLE_IMAGE_INVALID", `login_style.${field}: ${problem}`) : path;
514
+ return problem
515
+ ? c.fail("STANDALONE_LOGIN_STYLE_IMAGE_INVALID", `login_style.${field}: ${problem}`)
516
+ : path;
344
517
  };
345
- const [wallpaper, thumbnail, title] = [image("wallpaper"), image("wallpaper_thumbnail"), image("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 ? { wallpaper: { image: 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: { main: b => b.kind === "go" ? join(b.dir, b.main, "main.go") : "", call: "dsm.NewEntryGate(", sdk: `backend/go.mod must require ${SDK_MODULE}` },
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) && (!gate || !sdkVersionOrUndefined(backend))
380
- || gate && (!existsSync(main) || !readFileSync(main, "utf8").includes(gate.call))) {
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 ? read.settings : c.fail("STANDALONE_ROOT_PROGRAMS_INVALID", read.error);
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
- && value.length > 0
413
- && value.every((arch) => typeof arch === "string" && isNativeArchFamily(arch))
414
- && new Set(value).size === value.length;
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)) || (value.main !== undefined && (typeof value.main !== "string" || !GO_MAIN.test(value.main) || value.main.includes("..")))) {
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 = { kind: "go", dir, main: value.main ?? "./cmd/app" };
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) ? { ...backend, timeoutSeconds: timeout } : undefined;
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) || Object.keys(value).some(key => !["kind", "files"].includes(key))) {
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) ? { kind: "binary", files } : undefined;
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) || command.length === 0 || !command.every((arg) => typeof arg === "string" && arg !== "")) {
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 ? { dir, build: { cwd, command: command }, ...window } : undefined;
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) || Object.keys(value).some((key) => !I18N_FIELDS.has(key))) {
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 ? info && { info } : invalid("glossary needs messages.");
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 ? undefined : c.path(value.glossary, "i18n.glossary");
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 ? { catalogs: loaded.catalogs, info } : undefined;
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 === "webapi") {
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.`);