@synopackageland/cli 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +33 -18
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -846
  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/unsupported-types.js +1 -1
  14. package/dist/contract/validate.js +2 -2
  15. package/dist/contract/web-launch.d.ts +1 -12
  16. package/dist/contract/web-launch.js +10 -74
  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/e2e/dsm.d.ts +2 -2
  46. package/dist/e2e/dsm.js +10 -6
  47. package/dist/error-registry.js +1020 -834
  48. package/dist/host/share-permission-lookup.d.ts +1 -1
  49. package/dist/host/share-permission.d.ts +1 -1
  50. package/dist/host/share-permission.js +1 -1
  51. package/dist/info.js +120 -28
  52. package/dist/init.js +188 -64
  53. package/dist/inspect.js +21 -6
  54. package/dist/runtime/executor-digests.json +5 -5
  55. package/dist/skill.js +51 -9
  56. package/dist/standalone/api-description-inspect.d.ts +4 -0
  57. package/dist/standalone/api-description-inspect.js +12 -0
  58. package/dist/standalone/api-description.d.ts +7 -0
  59. package/dist/standalone/api-description.js +73 -0
  60. package/dist/standalone/build.d.ts +3 -0
  61. package/dist/standalone/build.js +140 -33
  62. package/dist/standalone/compose-errors.d.ts +3 -0
  63. package/dist/standalone/compose-errors.js +243 -0
  64. package/dist/standalone/compose.d.ts +97 -10
  65. package/dist/standalone/compose.js +306 -75
  66. package/dist/standalone/daemon.js +111 -29
  67. package/dist/standalone/dsm-window.js +39 -8
  68. package/dist/standalone/identity-gateway.d.ts +9 -2
  69. package/dist/standalone/identity-gateway.js +282 -66
  70. package/dist/standalone/inspect-spk.d.ts +12 -0
  71. package/dist/standalone/inspect-spk.js +666 -136
  72. package/dist/standalone/lifecycle.js +124 -42
  73. package/dist/standalone/manifest.d.ts +8 -1
  74. package/dist/standalone/manifest.js +303 -86
  75. package/dist/standalone/resource.js +6 -2
  76. package/dist/standalone/spk.d.ts +4 -1
  77. package/dist/standalone/spk.js +138 -39
  78. package/dist/template-paths.d.ts +0 -2
  79. package/dist/template-paths.js +0 -5
  80. package/dist/test-command.d.ts +1 -1
  81. package/dist/test-command.js +1 -1
  82. package/dist/validate.d.ts +3 -3
  83. package/dist/validate.js +32 -140
  84. package/package.json +5 -3
  85. package/schema/synopkgland-api-v1.schema.json +423 -0
  86. package/skills/SKILL.md +4 -2
  87. package/skills/reference/compose.md +349 -0
  88. package/skills/reference/create.md +9 -18
  89. package/skills/reference/ctl.md +41 -0
  90. package/skills/reference/deploy.md +1 -1
  91. package/skills/reference/files.md +5 -1
  92. package/skills/reference/identity-gateway.md +8 -144
  93. package/skills/reference/wrap.md +10 -39
  94. package/assets/go/daemon/fixture_linux_test.go +0 -15
  95. package/assets/go/daemon/fixture_other_test.go +0 -7
  96. package/assets/go/daemon/go.mod +0 -5
  97. package/assets/go/daemon/main.go +0 -636
  98. package/assets/go/daemon/main_test.go +0 -315
  99. package/assets/go/daemon/network.go +0 -107
  100. package/assets/go/daemon/network_test.go +0 -86
  101. package/assets/go/daemon/process_linux.go +0 -55
  102. package/assets/go/daemon/process_linux_test.go +0 -161
  103. package/assets/go/daemon/process_other.go +0 -14
  104. package/assets/go/daemon/testdata/child.go +0 -44
  105. package/assets/go/gateway/auth.go +0 -101
  106. package/assets/go/gateway/auth_test.go +0 -216
  107. package/assets/go/gateway/cgi.go +0 -133
  108. package/assets/go/gateway/cgi_test.go +0 -156
  109. package/assets/go/gateway/csrf.go +0 -73
  110. package/assets/go/gateway/csrf_test.go +0 -155
  111. package/assets/go/gateway/directory.go +0 -108
  112. package/assets/go/gateway/directory_test.go +0 -202
  113. package/assets/go/gateway/gateway.go +0 -412
  114. package/assets/go/gateway/gateway_test.go +0 -676
  115. package/assets/go/gateway/go.mod +0 -5
  116. package/assets/go/gateway/groups.go +0 -66
  117. package/assets/go/gateway/main.go +0 -45
  118. package/dist/build-spk/gateway.d.ts +0 -31
  119. package/dist/build-spk/gateway.js +0 -60
  120. package/dist/compose/privilege.d.ts +0 -14
  121. package/dist/compose/privilege.js +0 -102
@@ -1,40 +1,64 @@
1
1
  import { isWebLaunchMode } from "../contract/web-launch.js";
2
- import { validWebAPIProfiles } from '../runtime/webapi-profile.js';
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
- "schema", "package", "version", "build", "displayname", "description",
37
- "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",
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" || value.trim() === "" || /[\r\n"\\]/.test(value)) {
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 { 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
+ };
80
114
  }
81
115
  let raw;
82
116
  try {
83
117
  raw = parse(readFileSync(file, "utf8"));
84
118
  }
85
119
  catch (error) {
86
- 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
+ };
87
124
  }
88
125
  const c = new Collector(root);
89
- 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)))
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 ? { 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 };
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 ? 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);
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) && raw.build >= 1 && raw.build <= 9999
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 && osMinVer && !isDsmAtLeast(osMinVer, DEFAULT_MIN_DSM)) {
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) ? rootBinaryProblem(rootOps, rootPrograms, arches) : undefined;
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 = 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);
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.map(op => ({ op, setting: rootPrograms?.programs?.[op.name] })).filter(({ setting }) => !setting || setting.run)
154
- .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
+ }));
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 && readDaemon(raw.daemon, c.root, arches, e => c.fail(e.code, e.message));
176
- const compose = arches && readComposeApp(raw.compose, c.root, arches, e => c.fail(e.code, e.message));
177
- if (raw.compose !== undefined && (raw.daemon !== undefined || raw.identity_gateway === undefined || Object.keys(capabilities).length || !osMinVer || !isDsmAtLeast(osMinVer, "7.2.1-69057")))
178
- c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose requires identity_gateway, DSM >= 7.2.1, no daemon and no capabilities.");
179
- const identityGateway = readIdentityGateway(raw.identity_gateway, daemon || (compose ? { targets: compose.targets } : undefined), e => c.fail(e.code, e.message));
180
- 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))))
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 && raw.daemon !== undefined && capabilities.delegate?.lifetime !== 'until-revoked') {
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 && !(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)) {
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 ? undefined : readSharedFolder(raw.install, e => c.fail(e.code, e.message));
203
- 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);
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 = readUi(raw.ui, c);
212
- 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)) {
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) || 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)))
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 || Number(sdk.split('.')[0]) === 0 && Number(sdk.split('.')[1]) < 11)
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 ?? '0.0.0').split('.').map(Number);
231
- 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)))))
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, { rootOps: capabilities.rootOps === true, ops: rootOps, en: i18n?.catalogs?.en }, e => c.fail(e.code, e.message));
235
- const appScripts = readAppScripts(c.root, e => c.fail(e.code, e.message));
236
- const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
237
- ?? c.fail("STANDALONE_PORTAL_INVALID", "portal must be {} or { alias: <1-64 lowercase alphanumerics or -, not a DSM reserved path> }.");
238
- 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) {
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, { application: displayname, alias: portal.alias }) : undefined;
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 ? undefined : readLoginStyle(raw.login_style, raw.portal !== undefined, c);
249
- if (!packageId || !version || !build || !osMinVer || !displayname || !description || !maintainer
250
- || !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.");
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 ? { 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
+ : {}),
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 = ["color", "wallpaper", "wallpaper_thumbnail", "title"];
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 && (typeof value.color !== "string" || !LOGIN_COLOR.test(value.color))) {
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) !== (value.wallpaper_thumbnail === 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 ? 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;
345
517
  };
346
- 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
+ ];
347
523
  return {
348
524
  ...(value.color !== undefined ? { color: value.color } : {}),
349
- ...(wallpaper && thumbnail ? { wallpaper: { image: 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: { 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
+ },
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) && (!gate || !sdkVersionOrUndefined(backend))
381
- || 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)))) {
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 ? 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);
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
- && value.length > 0
414
- && value.every((arch) => typeof arch === "string" && isNativeArchFamily(arch))
415
- && 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;
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)) || (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("..")))) {
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 = { kind: "go", dir, main: value.main ?? "./cmd/app" };
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) ? { ...backend, timeoutSeconds: timeout } : undefined;
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) || Object.keys(value).some(key => !["kind", "files"].includes(key))) {
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) ? { kind: "binary", files } : undefined;
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) || 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 !== "")) {
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 ? { dir, build: { cwd, command: command }, ...launch, ...window } : undefined;
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) || Object.keys(value).some((key) => !I18N_FIELDS.has(key))) {
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 ? info && { info } : invalid("glossary needs messages.");
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 ? undefined : c.path(value.glossary, "i18n.glossary");
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 ? { catalogs: loaded.catalogs, info } : undefined;
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 === "webapi") {
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.`);