@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
@@ -0,0 +1,73 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { goChildEnv } from '../runtime/go-env.js';
3
+ import { makeDescriptor, validateDescriptor, flowMethods } from '../ctl/descriptor.js';
4
+ import { sdkBackend } from './manifest.js';
5
+ import { readApiPermissions } from './api-permissions.js';
6
+ import { isBridgeBackend } from './bridge-backend.js';
7
+ import { StandaloneBuildError } from './spk.js';
8
+ import { join } from 'node:path';
9
+ export async function buildApiDescription(m, bridgeMethods, run) {
10
+ if (!m.ctl?.enabled)
11
+ return undefined;
12
+ if (m.backend.kind === 'binary' || m.compose)
13
+ throw new StandaloneBuildError('CTL_UNSUPPORTED_BACKEND', 'Automatic API descriptions require a standard SDK native App.');
14
+ let methods;
15
+ const policy = readApiPermissions(sdkBackend(m), m.capabilities);
16
+ if (isBridgeBackend(m.backend)) {
17
+ methods = [...(bridgeMethods ?? []), ...Object.keys(m.apiPermissions?.methods ?? {}).filter(name => Object.values(policy.sdk).some(group => Object.hasOwn(group, name)))].map(method => ({ method, permission: m.apiPermissions?.methods[method] ?? 'administrators' }));
18
+ }
19
+ else {
20
+ if (!readFileSync(join(m.backend.dir, m.backend.main, 'main.go'), 'utf8').includes('--describe'))
21
+ throw new StandaloneBuildError('CTL_DESCRIBE_REQUIRED', 'Go CGI must support build-only --describe; update its template.');
22
+ const flags = [`-X main.packageID=${m.package}`, `-X main.version=${m.spkVersion}`, ...(m.capabilities.files || m.capabilities.delegate ? ['-X main.fileStation=session'] : []), ...(m.portal ? ['-X main.portal=on'] : [])].join(' ');
23
+ const response = await run('go', ['run', '-trimpath', '-buildvcs=false', '-ldflags', flags, m.backend.main, '--describe'], { cwd: m.backend.dir, env: goChildEnv({ ...process.env, GOOS: undefined, GOARCH: undefined, GOARM: undefined, CGO_ENABLED: '0' }), capture: true });
24
+ if (response.status !== 0)
25
+ throw new StandaloneBuildError('CTL_DESCRIBE_FAILED', 'Go route metadata export failed.');
26
+ try {
27
+ methods = JSON.parse(response.stdout ?? '');
28
+ }
29
+ catch {
30
+ throw new StandaloneBuildError('CTL_DESCRIBE_FAILED', 'Go route exporter returned invalid JSON.');
31
+ }
32
+ }
33
+ if (!Array.isArray(methods) || methods.some(item => typeof item.method !== 'string' || typeof item.permission !== 'string'))
34
+ throw new StandaloneBuildError('CTL_DESCRIBE_FAILED', 'Invalid registry snapshot.');
35
+ const registry = new Map(methods.map(item => [item.method, item.permission]));
36
+ // Identity transport routes are outside the business registry but have immutable SDK ownership.
37
+ if (m.capabilities.identity) {
38
+ registry.set('identity.session', 'public');
39
+ registry.set('identity.logout', 'public');
40
+ }
41
+ const bindings = {};
42
+ const commands = {};
43
+ const weights = { read: 0, write: 1, destructive: 2 };
44
+ for (const [id, declared] of Object.entries(m.ctl.commands)) {
45
+ const command = structuredClone(declared);
46
+ const names = flowMethods(command.flow);
47
+ let permission = 'public';
48
+ for (const name of names) {
49
+ if (!registry.has(name))
50
+ throw new StandaloneBuildError('CTL_COMMAND_UNREGISTERED', `Command ${id} references unregistered method ${name}.`);
51
+ const effective = m.entry === 'admin-only' ? 'administrators' : registry.get(name);
52
+ registry.set(name, effective);
53
+ const effect = command.flow.type === 'job' && name !== command.flow.start ? 'read' : command.effect;
54
+ const previous = bindings[name];
55
+ if (!previous || weights[effect] > weights[previous.effect])
56
+ bindings[name] = { permission: effective, effect };
57
+ if (effective === 'administrators' || effective === 'authenticated' && permission === 'public')
58
+ permission = effective;
59
+ }
60
+ commands[id] = { ...command, permission };
61
+ }
62
+ if (m.capabilities.identity) {
63
+ for (const method of ['identity.session', 'identity.logout'])
64
+ bindings[method] = { permission: 'public', effect: 'write' };
65
+ }
66
+ const description = makeDescriptor({ package: { id: m.package, version: m.spkVersion, displayName: m.displayname }, aliases: m.ctl.aliases ?? [m.package.toLowerCase()], provenance: { developedBy: m.ctl.developedBy ?? 'unknown', generator: 'synopkgland-cli' }, entry: m.entry ?? 'policy', transport: { path: `/webman/3rdparty/${m.package}/cgi/api.cgi`, auth: m.capabilities.identity ? 'dsm-app-session' : 'dsm-session' }, commands, bindings });
67
+ try {
68
+ return { description: validateDescriptor(description, m.package, m.spkVersion), registry: Object.fromEntries(registry) };
69
+ }
70
+ catch (error) {
71
+ throw new StandaloneBuildError('CTL_DESCRIPTOR_INVALID', error.message);
72
+ }
73
+ }
@@ -23,6 +23,9 @@ export interface StandaloneBuildOptions {
23
23
  /** Architectures built at once; defaults to 1. */
24
24
  jobs?: number;
25
25
  run?: CommandRunner;
26
+ /** Saved anonymous image manifests/config for an offline Compose rebuild. */
27
+ composeImageProofs?: Record<string, unknown>;
28
+ composeImageProofsByArch?: Partial<Record<NativeArchFamily, Record<string, unknown>>>;
26
29
  }
27
30
  /** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
28
31
  export declare const runCommand: CommandRunner;
@@ -1,14 +1,16 @@
1
+ import { buildApiDescription } from "./api-description.js";
1
2
  import { goChildEnv } from "../runtime/go-env.js";
2
3
  import { spawn } from "node:child_process";
3
- import { goModulePath, privateSdkHint, SDK_MODULE, sdkSource } from "../runtime/sdk-source.js";
4
+ import { goModulePath, privateSdkHint, SDK_MODULE, sdkSource, } from "../runtime/sdk-source.js";
4
5
  import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
5
6
  import { tmpdir } from "node:os";
6
7
  import { join } from "node:path";
7
8
  import { goBuildEnv } from "../build-spk/dsm-arch.js";
8
- import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
9
+ import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage, } from "../build-spk/dsm-version.js";
9
10
  import { checkCgiBinary } from "./binary.js";
10
- import { loadStandaloneManifest } from "./manifest.js";
11
+ import { loadStandaloneManifest, } from "./manifest.js";
11
12
  import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
13
+ import { buildComposeControl, lockComposeImages, dsmComposeProblem, } from "./compose.js";
12
14
  import { buildDaemonControl } from "./daemon.js";
13
15
  import { buildIdentityGateway } from "./identity-gateway.js";
14
16
  import { buildExecutor } from "../runtime/executor-build.js";
@@ -17,7 +19,7 @@ import { readApiPermissions } from "./api-permissions.js";
17
19
  import { checkJobs, commitOutputs, forEachArch } from "./arch-jobs.js";
18
20
  import { isBridgeBackend } from "./bridge-backend.js";
19
21
  import { requestTimeoutSeconds } from "./request-timeout.js";
20
- import { buildBridge, prepareBridge } from "./bridge-build.js";
22
+ import { buildBridge, prepareBridge, } from "./bridge-build.js";
21
23
  import { portalCatalogs } from "./portal-window.js";
22
24
  /** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
23
25
  export const runCommand = (command, args, options) => new Promise((done) => {
@@ -29,24 +31,37 @@ export const runCommand = (command, args, options) => new Promise((done) => {
29
31
  let stderr = "";
30
32
  let stdout = "";
31
33
  child.stdout?.setEncoding("utf8");
32
- child.stdout?.on("data", (chunk) => { stdout += chunk; });
34
+ child.stdout?.on("data", (chunk) => {
35
+ stdout += chunk;
36
+ });
33
37
  child.stderr.setEncoding("utf8");
34
38
  child.stderr.on("data", (chunk) => {
35
39
  stderr += chunk;
36
40
  process.stderr.write(chunk);
37
41
  });
38
42
  child.on("error", (error) => done({ status: null, stderr, missing: error.code === "ENOENT" }));
39
- child.on("close", (status) => done({ status, stderr, missing: false, ...(options.capture ? { stdout } : {}) }));
43
+ child.on("close", (status) => done({
44
+ status,
45
+ stderr,
46
+ missing: false,
47
+ ...(options.capture ? { stdout } : {}),
48
+ }));
40
49
  });
41
50
  /** `runCommand` for concurrent builds: every output line goes to stderr behind `prefix`, so architectures stay apart. */
42
51
  export function prefixedRunner(prefix) {
43
52
  return (command, args, options) => new Promise((done) => {
44
- const child = spawn(command, args, { cwd: options.cwd, env: options.env, stdio: ["ignore", "pipe", "pipe"] });
53
+ const child = spawn(command, args, {
54
+ cwd: options.cwd,
55
+ env: options.env,
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ });
45
58
  let stderr = "";
46
59
  let stdout = "";
47
60
  if (options.capture) {
48
61
  child.stdout.setEncoding("utf8");
49
- child.stdout.on("data", (chunk) => { stdout += chunk; });
62
+ child.stdout.on("data", (chunk) => {
63
+ stdout += chunk;
64
+ });
50
65
  }
51
66
  const relay = (stream, keep) => {
52
67
  let partial = "";
@@ -59,14 +74,21 @@ export function prefixedRunner(prefix) {
59
74
  for (const line of lines)
60
75
  process.stderr.write(`${prefix}${line}\n`);
61
76
  });
62
- stream.on("end", () => { if (partial)
63
- process.stderr.write(`${prefix}${partial}\n`); });
77
+ stream.on("end", () => {
78
+ if (partial)
79
+ process.stderr.write(`${prefix}${partial}\n`);
80
+ });
64
81
  };
65
82
  if (!options.capture)
66
83
  relay(child.stdout, false);
67
84
  relay(child.stderr, true);
68
85
  child.on("error", (error) => done({ status: null, stderr, missing: error.code === "ENOENT" }));
69
- child.on("close", (status) => done({ status, stderr, missing: false, ...(options.capture ? { stdout } : {}) }));
86
+ child.on("close", (status) => done({
87
+ status,
88
+ stderr,
89
+ missing: false,
90
+ ...(options.capture ? { stdout } : {}),
91
+ }));
70
92
  });
71
93
  }
72
94
  /** The runner one architecture uses: the caller's, or a labelled one while several architectures share the terminal. */
@@ -83,14 +105,19 @@ export async function buildStandalone(options) {
83
105
  const manifest = options.minDsm === undefined
84
106
  ? loaded.manifest
85
107
  : { ...loaded.manifest, os_min_ver: resolveMinDsm(options.minDsm) };
86
- if (manifest.capabilities.rootOps === true && !isDsmAtLeast(manifest.os_min_ver, DEFAULT_MIN_DSM)) {
108
+ if (manifest.capabilities.rootOps === true &&
109
+ !isDsmAtLeast(manifest.os_min_ver, DEFAULT_MIN_DSM)) {
87
110
  throw new StandaloneBuildError("STANDALONE_OS_MIN_VER_INVALID", `root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
88
111
  }
89
- if (manifest.portal && !isDsmAtLeast(manifest.os_min_ver, PORTAL_EXECUTOR_MIN_DSM)) {
112
+ if (manifest.portal &&
113
+ !isDsmAtLeast(manifest.os_min_ver, PORTAL_EXECUTOR_MIN_DSM)) {
90
114
  throw new StandaloneBuildError("STANDALONE_PORTAL_MIN_DSM", `[STANDALONE_PORTAL_MIN_DSM] ${portalMinDsmMessage(manifest.os_min_ver, "portal")}`);
91
115
  }
92
116
  if (manifest.compose && !isDsmAtLeast(manifest.os_min_ver, "7.2.1-69057"))
93
117
  throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose identity_gateway requires DSM >= 7.2.1.");
118
+ const composeProblem = manifest.compose && dsmComposeProblem(manifest.compose);
119
+ if (composeProblem)
120
+ throw new StandaloneBuildError(composeProblem.code, composeProblem.message);
94
121
  const jobs = checkJobs(options.jobs);
95
122
  const arches = options.arches ?? manifest.arches;
96
123
  for (const arch of arches) {
@@ -104,20 +131,42 @@ export async function buildStandalone(options) {
104
131
  const warnings = new Set();
105
132
  try {
106
133
  const bridge = await prepareBridge(manifest, work);
134
+ const apiArtifact = await buildApiDescription(manifest, bridge?.methods, options.run ?? runCommand);
135
+ const apiDescription = apiArtifact?.description;
136
+ const sourceManifest = manifest;
107
137
  const outputs = await commitOutputs(outDir, (stage) => forEachArch(arches, jobs, async (arch) => {
138
+ const manifest = lockComposeImages(sourceManifest, arch, true, options.composeImageProofsByArch?.[arch] ??
139
+ options.composeImageProofs);
108
140
  const run = runnerFor(options.run, jobs, arch);
109
141
  const archWork = join(work, arch);
110
142
  mkdirSync(archWork);
111
143
  const executor = await buildExecutor(manifest, arch, archWork, run);
112
- executor?.warnings.forEach(warning => warnings.add(warning));
144
+ executor?.warnings.forEach((warning) => warnings.add(warning));
113
145
  const builtManifest = executor ? { ...manifest, executor } : manifest;
114
- const { cgi: cgiPath, worker: backgroundWorker, app } = await backendFor(builtManifest, arch, archWork, run, bridge);
146
+ const { cgi: cgiPath, worker: backgroundWorker, app, } = await backendFor(builtManifest, arch, archWork, run, bridge);
115
147
  const outputPath = join(stage, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
116
148
  const daemonControl = await buildDaemonControl(manifest, arch, archWork, run);
117
149
  const identityGateway = await buildIdentityGateway(manifest, arch, archWork, run);
150
+ const composeControl = await buildComposeControl(manifest, arch, archWork, run);
118
151
  // A portal needs the executor, so the executor's SDK carries the panel text.
119
- const portalText = manifest.portal && executor ? portalCatalogs(sdkSource(executor.sdk).goDir) : undefined;
120
- await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, identityGateway, backgroundWorker, executor, app, portalText });
152
+ const portalText = manifest.portal && executor
153
+ ? portalCatalogs(sdkSource(executor.sdk).goDir)
154
+ : undefined;
155
+ await assembleStandaloneSpk({
156
+ manifest: builtManifest,
157
+ arch,
158
+ cgiPath,
159
+ outputPath,
160
+ daemonControl,
161
+ identityGateway,
162
+ composeControl,
163
+ backgroundWorker,
164
+ executor,
165
+ app,
166
+ portalText,
167
+ apiDescription,
168
+ apiRegistry: apiArtifact?.registry,
169
+ });
121
170
  return outputPath;
122
171
  }));
123
172
  return { outputs, warnings: [...warnings] };
@@ -132,9 +181,14 @@ async function buildUi(manifest, run) {
132
181
  return;
133
182
  }
134
183
  const [command, ...args] = build.command;
135
- const result = await run(command, args, { cwd: build.cwd, env: { ...process.env } });
184
+ const result = await run(command, args, {
185
+ cwd: build.cwd,
186
+ env: { ...process.env },
187
+ });
136
188
  if (result.missing || result.status !== 0) {
137
- const installHint = command === "npm" && existsSync(join(build.cwd, "package.json")) && !existsSync(join(build.cwd, "node_modules"))
189
+ const installHint = command === "npm" &&
190
+ existsSync(join(build.cwd, "package.json")) &&
191
+ !existsSync(join(build.cwd, "node_modules"))
138
192
  ? `\nThe UI dependencies are not installed. Run npm ${existsSync(join(build.cwd, "package-lock.json")) ? "ci" : "install"} in ${build.cwd}, then run build again.`
139
193
  : "";
140
194
  throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build (${build.command.join(" ")}) failed. ${installHint}\n${result.stderr}`);
@@ -143,19 +197,29 @@ async function buildUi(manifest, run) {
143
197
  /** The checked CGI of one architecture and, with capabilities.jobs, its fixed worker; or the runtime and App of a bridge App. */
144
198
  async function backendFor(manifest, arch, work, run, bridge) {
145
199
  const backend = manifest.backend;
200
+ if (manifest.compose)
201
+ return {};
146
202
  if (isBridgeBackend(backend))
147
203
  return buildBridge(manifest, bridge, arch, work, run);
148
- const cgi = backend.kind === "binary" ? backend.files[arch] : await buildGoCgi(manifest, backend, arch, work, run);
204
+ const cgi = backend.kind === "binary"
205
+ ? backend.files[arch]
206
+ : await buildGoCgi(manifest, backend, arch, work, run);
149
207
  const problem = checkCgiBinary(backend.kind === "go" ? work : manifest.root, cgi, arch);
150
208
  if (problem) {
151
209
  throw new StandaloneBuildError(problem.code, problem.message);
152
210
  }
153
- return { cgi, worker: await buildBackgroundWorker(manifest, arch, work, run) };
211
+ return {
212
+ cgi,
213
+ worker: await buildBackgroundWorker(manifest, arch, work, run),
214
+ };
154
215
  }
155
216
  async function buildGoCgi(manifest, backend, arch, work, run) {
156
217
  // UI build hooks run after validation. Refuse policy changes rather than
157
218
  // compiling CGI permissions different from the launcher metadata snapshot.
158
- if (readApiPermissions(backend, manifest.capabilities).digest !== manifest.apiPermissionsDigest || readApiPermissions(backend, manifest.capabilities).sdkDigest !== manifest.apiSDKPermissionsDigest) {
219
+ if (readApiPermissions(backend, manifest.capabilities).digest !==
220
+ manifest.apiPermissionsDigest ||
221
+ readApiPermissions(backend, manifest.capabilities).sdkDigest !==
222
+ manifest.apiSDKPermissionsDigest) {
159
223
  throw new StandaloneBuildError("STANDALONE_API_PERMISSIONS_CHANGED", "API permissions changed during build; check and rebuild.");
160
224
  }
161
225
  const module = goModulePath(backend.dir);
@@ -172,13 +236,35 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
172
236
  `-X ${module}/internal/api.expectedPermissionsDigest=${manifest.apiPermissionsDigest}`,
173
237
  `-X ${SDK_MODULE}/api.expectedSDKPermissionsDigest=${manifest.apiSDKPermissionsDigest}`,
174
238
  ...delegateBuildFlags(manifest),
175
- ...(manifest.executor ? [`-X main.executorDigest=${manifest.executor.executorDigest}`, `-X main.policyDigest=${manifest.executor.policyDigest}`] : []),
176
- ...(manifest.capabilities.identity ? [`-X main.identity=${manifest.capabilities.identity}`] : []),
177
- ...(manifest.capabilities.files || manifest.capabilities.delegate ? [`-X main.fileStation=session`] : []),
239
+ ...(manifest.executor
240
+ ? [
241
+ `-X main.executorDigest=${manifest.executor.executorDigest}`,
242
+ `-X main.policyDigest=${manifest.executor.policyDigest}`,
243
+ ]
244
+ : []),
245
+ ...(manifest.capabilities.identity
246
+ ? [`-X main.identity=${manifest.capabilities.identity}`]
247
+ : []),
248
+ ...(manifest.capabilities.files || manifest.capabilities.delegate
249
+ ? [`-X main.fileStation=session`]
250
+ : []),
178
251
  ...(manifest.entry ? [`-X main.entry=${manifest.entry}`] : []),
179
- ...(manifest.portal ? [`-X main.portal=${portalByExecutor(manifest.os_min_ver) ? "executor" : "on"}`] : []),
252
+ ...(manifest.portal
253
+ ? [
254
+ `-X main.portal=${portalByExecutor(manifest.os_min_ver) ? "executor" : "on"}`,
255
+ ]
256
+ : []),
180
257
  ].join(" ");
181
- const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, backend.main], {
258
+ const result = await run("go", [
259
+ "build",
260
+ "-trimpath",
261
+ "-buildvcs=false",
262
+ "-ldflags",
263
+ ldflags,
264
+ "-o",
265
+ out,
266
+ backend.main,
267
+ ], {
182
268
  cwd: backend.dir,
183
269
  env: goChildEnv({ ...process.env, ...goBuildEnv(arch) }),
184
270
  });
@@ -186,7 +272,9 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
186
272
  throw new StandaloneBuildError("STANDALONE_GO_MISSING", "The go command was not found. Install Go 1.25 or later (https://go.dev/dl/), or package an existing executable with backend.binary.");
187
273
  }
188
274
  if (result.status !== 0) {
189
- const fetchingSdk = result.stderr.includes(`${SDK_MODULE}@`) ? privateSdkHint() : "";
275
+ const fetchingSdk = result.stderr.includes(`${SDK_MODULE}@`)
276
+ ? privateSdkHint()
277
+ : "";
190
278
  throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `go build (${arch}) failed.\n${result.stderr}${fetchingSdk}`);
191
279
  }
192
280
  return out;
@@ -196,10 +284,29 @@ async function buildBackgroundWorker(manifest, arch, work, run) {
196
284
  if (!manifest.capabilities.jobs || manifest.backend.kind !== "go")
197
285
  return undefined;
198
286
  const out = join(work, `background-worker-${arch}`);
199
- const ldflags = ["-s", "-w", "-buildid=", `-X main.packageID=${manifest.package}`, ...delegateBuildFlags(manifest),
200
- ...(manifest.executor ? [`-X main.executorDigest=${manifest.executor.executorDigest}`, `-X main.policyDigest=${manifest.executor.policyDigest}`] : []),
287
+ const ldflags = [
288
+ "-s",
289
+ "-w",
290
+ "-buildid=",
291
+ `-X main.packageID=${manifest.package}`,
292
+ ...delegateBuildFlags(manifest),
293
+ ...(manifest.executor
294
+ ? [
295
+ `-X main.executorDigest=${manifest.executor.executorDigest}`,
296
+ `-X main.policyDigest=${manifest.executor.policyDigest}`,
297
+ ]
298
+ : []),
201
299
  ].join(" ");
202
- const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, BACKGROUND_WORKER_MAIN], {
300
+ const result = await run("go", [
301
+ "build",
302
+ "-trimpath",
303
+ "-buildvcs=false",
304
+ "-ldflags",
305
+ ldflags,
306
+ "-o",
307
+ out,
308
+ BACKGROUND_WORKER_MAIN,
309
+ ], {
203
310
  cwd: manifest.backend.dir,
204
311
  env: goChildEnv({ ...process.env, ...goBuildEnv(arch) }),
205
312
  });
@@ -219,7 +326,7 @@ function delegateBuildFlags(manifest) {
219
326
  delegateScope: d.scope,
220
327
  delegateOperationsJSON: JSON.stringify([...d.operations].sort()),
221
328
  delegateDestinationsJSON: JSON.stringify(d.destinations ? [...d.destinations].sort() : []),
222
- delegateLifetime: d.lifetime ?? '',
329
+ delegateLifetime: d.lifetime ?? "",
223
330
  };
224
331
  // Go's quoted.Split does not process backslash escapes. Interior JSON quotes
225
332
  // in a whitespace-free argument are literal; never shell-escape this string.
@@ -231,6 +338,6 @@ function delegateBuildFlags(manifest) {
231
338
  return `-X '${arg}'`;
232
339
  if (!arg.includes('"'))
233
340
  return `-X "${arg}"`;
234
- throw new StandaloneBuildError('STANDALONE_GO_BUILD_FAILED', 'Delegate scope with whitespace and both quote types cannot be represented in Go linker flags.');
341
+ throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", "Delegate scope with whitespace and both quote types cannot be represented in Go linker flags.");
235
342
  });
236
343
  }
@@ -0,0 +1,3 @@
1
+ import type { ErrorCodeEntry } from "../error-registry.js";
2
+ /** SDK planner errors accepted at this versioned tool boundary. */
3
+ export declare const COMPOSE_ERRORS: Record<string, ErrorCodeEntry>;
@@ -0,0 +1,243 @@
1
+ /** SDK planner errors accepted at this versioned tool boundary. */
2
+ export const COMPOSE_ERRORS = {
3
+ COMPOSE_SUBNET_STATE_INVALID: {
4
+ code: "COMPOSE_SUBNET_STATE_INVALID",
5
+ description: "The private ingress history is invalid or has foreign ownership.",
6
+ suggestion: "Inspect the package private state; do not bypass route checks or claim network ownership.",
7
+ },
8
+ COMPOSE_SUBNET_INVALID: {
9
+ code: "COMPOSE_SUBNET_INVALID",
10
+ description: "Invalid ingress subnet candidate.",
11
+ suggestion: "Set compose.network.ingress_candidate to an integer from 0 through 255.",
12
+ },
13
+ COMPOSE_SUBNET_CONFLICT: {
14
+ code: "COMPOSE_SUBNET_CONFLICT",
15
+ description: "The fixed ingress subnet overlaps a NAS route.",
16
+ suggestion: "Choose a different compose.network.ingress_candidate and rebuild; do not change NAS networks.",
17
+ },
18
+ COMPOSE_SUBNET_INVENTORY_UNAVAILABLE: {
19
+ code: "COMPOSE_SUBNET_INVENTORY_UNAVAILABLE",
20
+ description: "Kernel route inventory is unavailable.",
21
+ suggestion: "Use a supported Linux package-account environment; never borrow Docker socket or administrator credentials.",
22
+ },
23
+ COMPOSE_PRIVATE_BIND_TIMING_UNSUPPORTED: {
24
+ code: "COMPOSE_PRIVATE_BIND_TIMING_UNSUPPORTED",
25
+ description: "DSM private-bind initialization is unavailable before official worker acquire.",
26
+ suggestion: "Use an explicitly selected arbitrary-UID-compatible named volume or keep the configuration unsupported; never initialize pkgvar as root.",
27
+ },
28
+ COMPOSE_HEALTH_REQUIRED: {
29
+ code: "COMPOSE_HEALTH_REQUIRED",
30
+ description: "Bridge ingress requires a declared App health endpoint.",
31
+ suggestion: "Set compose.web.health.path and its expected 2xx status.",
32
+ },
33
+ COMPOSE_READINESS_INVALID: {
34
+ code: "COMPOSE_READINESS_INVALID",
35
+ description: "Bridge readiness configuration is invalid.",
36
+ suggestion: "Rebuild with the matching SDK and declare a local health path and expected 2xx status.",
37
+ },
38
+ COMPOSE_ACQUIRE_GENERATION_INVALID: {
39
+ code: "COMPOSE_ACQUIRE_GENERATION_INVALID",
40
+ description: "The lifecycle acquire generation is missing, released or stale.",
41
+ suggestion: "Start the installed package through Package Center; this record does not attest Docker identity.",
42
+ },
43
+ COMPOSE_TCP_NOT_READY: {
44
+ code: "COMPOSE_TCP_NOT_READY",
45
+ description: "The declared ingress TCP endpoint is unavailable.",
46
+ suggestion: "Inspect the official project's startup result; do not expose upstream ports to the LAN.",
47
+ },
48
+ COMPOSE_HEALTH_NOT_READY: {
49
+ code: "COMPOSE_HEALTH_NOT_READY",
50
+ description: "The App health endpoint did not return the expected status.",
51
+ suggestion: "Check the declared App health path/status and upstream health; redirects are not followed.",
52
+ },
53
+ COMPOSE_REQUEST_INVALID: {
54
+ code: "COMPOSE_REQUEST_INVALID",
55
+ description: "The unified Compose request is invalid.",
56
+ suggestion: "Declare schema-2 files, web and supported DSM differences; see the Compose reference.",
57
+ },
58
+ COMPOSE_PROJECT_CONTROL_UNAVAILABLE: {
59
+ code: "COMPOSE_PROJECT_CONTROL_UNAVAILABLE",
60
+ description: "Protected official Compose project control is unavailable.",
61
+ suggestion: "Keep the project disabled until an official protected ACL/project Adapter has been verified; do not use host root or a destination grant.",
62
+ },
63
+ COMPOSE_ANONYMOUS_VOLUME_UNSUPPORTED: {
64
+ code: "COMPOSE_ANONYMOUS_VOLUME_UNSUPPORTED",
65
+ description: "The declared value:the declared value needs a stable declared volume.",
66
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
67
+ },
68
+ COMPOSE_BASE_PATH_UNSUPPORTED: {
69
+ code: "COMPOSE_BASE_PATH_UNSUPPORTED",
70
+ description: "Use the upstream's dedicated origin at /; automatic subpath asset rewriting is not supported.",
71
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
72
+ },
73
+ COMPOSE_BUILD_UNSUPPORTED: {
74
+ code: "COMPOSE_BUILD_UNSUPPORTED",
75
+ description: "The declared value must use an upstream prebuilt image.",
76
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
77
+ },
78
+ COMPOSE_EGRESS_ALLOWLIST_UNSUPPORTED: {
79
+ code: "COMPOSE_EGRESS_ALLOWLIST_UNSUPPORTED",
80
+ description: "Choose isolated or internet; destination allowlists are not implemented.",
81
+ suggestion: "Declare isolated or internet; destination allowlists are unsupported.",
82
+ },
83
+ COMPOSE_ENV_INVALID: {
84
+ code: "COMPOSE_ENV_INVALID",
85
+ description: "Invalid service environment.",
86
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
87
+ },
88
+ COMPOSE_EXTERNAL_VOLUME_UNVERIFIED: {
89
+ code: "COMPOSE_EXTERNAL_VOLUME_UNVERIFIED",
90
+ description: "External volume the declared value needs trusted admin ownership/driver readback; no official controller is configured.",
91
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
92
+ },
93
+ COMPOSE_FILE_RESOURCE_UNSUPPORTED: {
94
+ code: "COMPOSE_FILE_RESOURCE_UNSUPPORTED",
95
+ description: "Secret/config resource delivery is not implemented; use explicit package-owned configuration.",
96
+ suggestion: "Use bounded regular project-local files without symlink or remote dependencies, then rebuild from unchanged bytes.",
97
+ },
98
+ COMPOSE_IMAGE_INVALID: {
99
+ code: "COMPOSE_IMAGE_INVALID",
100
+ description: "Service the declared value needs an image.",
101
+ suggestion: "Use the intended anonymous registry repository and verified Linux platform digest; recheck source/image proofs and rebuild.",
102
+ },
103
+ COMPOSE_IMAGE_LOCK_INVALID: {
104
+ code: "COMPOSE_IMAGE_LOCK_INVALID",
105
+ description: "Unsafe image VOLUME target.",
106
+ suggestion: "Use the intended anonymous registry repository and verified Linux platform digest; recheck source/image proofs and rebuild.",
107
+ },
108
+ COMPOSE_IMAGE_PLATFORM_UNSUPPORTED: {
109
+ code: "COMPOSE_IMAGE_PLATFORM_UNSUPPORTED",
110
+ description: "The declared value has no Linux the declared value image.",
111
+ suggestion: "Use the intended anonymous registry repository and verified Linux platform digest; recheck source/image proofs and rebuild.",
112
+ },
113
+ COMPOSE_IMAGE_RESOLVE_FAILED: {
114
+ code: "COMPOSE_IMAGE_RESOLVE_FAILED",
115
+ description: "Registry mirror must be an HTTPS origin.",
116
+ suggestion: "Use the intended anonymous registry repository and verified Linux platform digest; recheck source/image proofs and rebuild.",
117
+ },
118
+ COMPOSE_INPUT_CHANGED: {
119
+ code: "COMPOSE_INPUT_CHANGED",
120
+ description: "Compose input bytes changed after planning.",
121
+ suggestion: "Use bounded regular project-local files without symlink or remote dependencies, then rebuild from unchanged bytes.",
122
+ },
123
+ COMPOSE_LEGACY_MANIFEST_REMOVED: {
124
+ code: "COMPOSE_LEGACY_MANIFEST_REMOVED",
125
+ description: "Compose requires a schema-2 dsm-package.yaml declaration.",
126
+ suggestion: "Run init -t compose and declare differences in schema-2 dsm-package.yaml while retaining upstream bytes.",
127
+ },
128
+ COMPOSE_LOCK_INVALID: {
129
+ code: "COMPOSE_LOCK_INVALID",
130
+ description: "Private storage inventory is invalid.",
131
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
132
+ },
133
+ COMPOSE_MODEL_INVALID: {
134
+ code: "COMPOSE_MODEL_INVALID",
135
+ description: "The declared value.",
136
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
137
+ },
138
+ COMPOSE_NETWORK_UNSUPPORTED: {
139
+ code: "COMPOSE_NETWORK_UNSUPPORTED",
140
+ description: "Network the declared value must be a managed bridge without custom driver/IPAM options.",
141
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
142
+ },
143
+ COMPOSE_OIDC_TOPOLOGY_UNSUPPORTED: {
144
+ code: "COMPOSE_OIDC_TOPOLOGY_UNSUPPORTED",
145
+ description: "Host service the declared value requires oidc Web ingress and a verified address_env.",
146
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
147
+ },
148
+ COMPOSE_PACKAGE_INVALID: {
149
+ code: "COMPOSE_PACKAGE_INVALID",
150
+ description: "Package must be 2-64 alphanumerics starting with a letter.",
151
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
152
+ },
153
+ COMPOSE_PRIVILEGE_UNSUPPORTED: {
154
+ code: "COMPOSE_PRIVILEGE_UNSUPPORTED",
155
+ description: "Container root capability the declared value is not supported.",
156
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
157
+ },
158
+ COMPOSE_ROOT_FORBIDDEN: {
159
+ code: "COMPOSE_ROOT_FORBIDDEN",
160
+ description: "Prepare must run as the package account.",
161
+ suggestion: "Use the package identity; container-root requires explicit reason, bridge and no host bind. Never elevate the App on the host.",
162
+ },
163
+ COMPOSE_ROOT_HOST_MOUNT_FORBIDDEN: {
164
+ code: "COMPOSE_ROOT_HOST_MOUNT_FORBIDDEN",
165
+ description: "Container-root service the declared value cannot bind host files or Shares.",
166
+ suggestion: "Use the package identity; container-root requires explicit reason, bridge and no host bind. Never elevate the App on the host.",
167
+ },
168
+ COMPOSE_ROOT_OPT_IN_INVALID: {
169
+ code: "COMPOSE_ROOT_OPT_IN_INVALID",
170
+ description: "Container root needs a reason and bridge networking.",
171
+ suggestion: "Use the package identity; container-root requires explicit reason, bridge and no host bind. Never elevate the App on the host.",
172
+ },
173
+ COMPOSE_RUN_AS_INVALID: {
174
+ code: "COMPOSE_RUN_AS_INVALID",
175
+ description: "Service the declared value run_as must be package or container-root.",
176
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
177
+ },
178
+ COMPOSE_SCALE_UNSUPPORTED: {
179
+ code: "COMPOSE_SCALE_UNSUPPORTED",
180
+ description: "The declared value deploy/replicas need an explicit local runtime contract.",
181
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
182
+ },
183
+ COMPOSE_SERVICE_UNKNOWN: {
184
+ code: "COMPOSE_SERVICE_UNKNOWN",
185
+ description: "Services.the declared value is not active.",
186
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
187
+ },
188
+ COMPOSE_SHARE_INVALID: {
189
+ code: "COMPOSE_SHARE_INVALID",
190
+ description: "Shares.the declared value needs admin selection, ro/rw and fixed mounts.",
191
+ suggestion: "Keep the project disabled until an official protected mount/project adapter is verified; never use a destination grant or host root workaround.",
192
+ },
193
+ COMPOSE_SHARE_NEEDS_APPROVAL: {
194
+ code: "COMPOSE_SHARE_NEEDS_APPROVAL",
195
+ description: "Declare capabilities.composeMounts: true; user destination grants cannot authorize package mounts.",
196
+ suggestion: "Keep the project disabled until an official protected mount/project adapter is verified; never use a destination grant or host root workaround.",
197
+ },
198
+ COMPOSE_SOURCE_UNSAFE: {
199
+ code: "COMPOSE_SOURCE_UNSAFE",
200
+ description: "Files must contain 1-32 local Compose files.",
201
+ suggestion: "Use bounded regular project-local files without symlink or remote dependencies, then rebuild from unchanged bytes.",
202
+ },
203
+ COMPOSE_STORAGE_INVALID: {
204
+ code: "COMPOSE_STORAGE_INVALID",
205
+ description: "Data.the declared value needs an active service, target, storage: package and safe path.",
206
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
207
+ },
208
+ COMPOSE_STORAGE_NOT_READY: {
209
+ code: "COMPOSE_STORAGE_NOT_READY",
210
+ description: "Invalid package var path.",
211
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
212
+ },
213
+ COMPOSE_STORAGE_UNMAPPED: {
214
+ code: "COMPOSE_STORAGE_UNMAPPED",
215
+ description: "The declared value:the declared value needs package data or an approved Share mapping.",
216
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
217
+ },
218
+ COMPOSE_VOLUME_DRIVER_UNSUPPORTED: {
219
+ code: "COMPOSE_VOLUME_DRIVER_UNSUPPORTED",
220
+ description: "Reserved image volume namespace collision.",
221
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
222
+ },
223
+ COMPOSE_WEB_INVALID: {
224
+ code: "COMPOSE_WEB_INVALID",
225
+ description: "Compose web ingress is invalid.",
226
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
227
+ },
228
+ SDK_COMPOSE_BUILD_FAILED: {
229
+ code: "SDK_COMPOSE_BUILD_FAILED",
230
+ description: "The trusted SDK Compose tool could not be compiled.",
231
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
232
+ },
233
+ SDK_COMPOSE_FAILED: {
234
+ code: "SDK_COMPOSE_FAILED",
235
+ description: "The SDK Compose tool could not return a valid plan.",
236
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
237
+ },
238
+ SDK_COMPOSE_UNSUPPORTED: {
239
+ code: "SDK_COMPOSE_UNSUPPORTED",
240
+ description: "This SDK release does not contain the unified Compose planner.",
241
+ suggestion: "Correct the reported declaration in dsm-package.yaml; see the Compose support matrix.",
242
+ },
243
+ };