@synopackageland/cli 0.17.3 → 0.17.5

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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +37 -1
  3. package/dist/build-spk/compose-diagnosis.d.ts +4 -0
  4. package/dist/build-spk/compose-diagnosis.js +12 -0
  5. package/dist/build-spk/compose-retained-data.d.ts +11 -0
  6. package/dist/build-spk/compose-retained-data.js +34 -0
  7. package/dist/build-spk/delete-app-data.d.ts +1 -1
  8. package/dist/build-spk/delete-app-data.js +1 -1
  9. package/dist/build-spk/uninstall-wizard.d.ts +2 -0
  10. package/dist/build-spk/uninstall-wizard.js +2 -1
  11. package/dist/build-spk/wizard-messages.d.ts +154 -0
  12. package/dist/build-spk/wizard-messages.js +154 -0
  13. package/dist/ctl/cli.js +10 -6
  14. package/dist/ctl/flows.js +1 -1
  15. package/dist/ctl/service.d.ts +4 -1
  16. package/dist/ctl/service.js +20 -6
  17. package/dist/ctl/transfers.d.ts +1 -1
  18. package/dist/ctl/transfers.js +5 -2
  19. package/dist/ctl/transport.d.ts +2 -1
  20. package/dist/ctl/transport.js +7 -3
  21. package/dist/runtime/compose-data.d.ts +10 -0
  22. package/dist/runtime/compose-data.js +20 -0
  23. package/dist/runtime/executor-digests.json +22 -0
  24. package/dist/runtime/executor-policy.d.ts +25 -0
  25. package/dist/runtime/executor-policy.js +10 -1
  26. package/dist/runtime/runtime-budgets.d.ts +24 -0
  27. package/dist/runtime/runtime-budgets.js +13 -0
  28. package/dist/standalone/background.d.ts +3 -3
  29. package/dist/standalone/background.js +12 -11
  30. package/dist/standalone/compose-errors.js +10 -0
  31. package/dist/standalone/compose.d.ts +5 -2
  32. package/dist/standalone/compose.js +19 -2
  33. package/dist/standalone/daemon.d.ts +4 -0
  34. package/dist/standalone/daemon.js +36 -23
  35. package/dist/standalone/dsm-window.js +2 -1
  36. package/dist/standalone/identity-gateway.js +9 -1
  37. package/dist/standalone/inspect-spk.js +87 -15
  38. package/dist/standalone/lifecycle.js +24 -10
  39. package/dist/standalone/manifest.d.ts +1 -0
  40. package/dist/standalone/manifest.js +8 -5
  41. package/dist/standalone/native-gateway.d.ts +17 -0
  42. package/dist/standalone/native-gateway.js +115 -0
  43. package/dist/standalone/resource.d.ts +3 -0
  44. package/dist/standalone/resource.js +3 -1
  45. package/dist/standalone/spk.js +14 -3
  46. package/dist/standalone/uninstall-hook.js +21 -8
  47. package/dist/standalone/uninstall.d.ts +1 -1
  48. package/dist/standalone/uninstall.js +2 -2
  49. package/package.json +2 -2
  50. package/skills/reference/compose.md +53 -0
  51. package/skills/reference/create.md +1 -1
  52. package/skills/reference/lifecycle.md +25 -0
@@ -1,5 +1,6 @@
1
+ import { operationBudgetSeconds, runtimeBudgetSdkSupported } from "../runtime/runtime-budgets.js";
1
2
  import { goChildEnv } from "../runtime/go-env.js";
2
- import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
3
+ import { chmodSync, existsSync, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
3
4
  import { isAbsolute, join, relative, resolve } from "node:path";
4
5
  import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem, } from "../build-spk/go-toolchain.js";
5
6
  import { sdkSource, appSdkVersion } from "../runtime/sdk-source.js";
@@ -79,23 +80,9 @@ export function readDaemon(value, root, arches, fail) {
79
80
  Object.keys(env).length > 64 ||
80
81
  Object.entries(env).some(([k, v]) => !envKey(k) || !token(v)))
81
82
  return invalid();
82
- const limits = value.timeouts ?? {};
83
- if (!record(limits) ||
84
- Object.keys(limits).some((k) => !["start", "stop", "status", "uninstall"].includes(k)))
85
- return invalid();
86
- // Defaults hold under IO load on a busy NAS; DSM's package unit allows 3600 s.
87
- const timeouts = {
88
- start: limits.start ?? 30,
89
- stop: limits.stop ?? 30,
90
- status: limits.status ?? 5,
91
- uninstall: limits.uninstall ?? 60,
92
- };
93
- for (const [key, v] of Object.entries(timeouts)) {
94
- if (!Number.isInteger(v) ||
95
- v < (key === "status" ? 1 : 2) ||
96
- v > (key === "status" ? 10 : 120))
97
- return invalid();
98
- }
83
+ const timeouts = readLifecycleTimeouts(value.timeouts, fail);
84
+ if (!timeouts)
85
+ return undefined;
99
86
  return {
100
87
  exec: value.exec,
101
88
  targets,
@@ -107,6 +94,11 @@ export function readDaemon(value, root, arches, fail) {
107
94
  timeouts: timeouts,
108
95
  };
109
96
  }
97
+ function daemonSdkVersion(manifest) {
98
+ return manifest.identityGateway?.sdk ??
99
+ (manifest.backend.kind === "go" ? appSdkVersion(manifest.backend) : undefined) ??
100
+ templateSdkVersion();
101
+ }
110
102
  export function daemonConfig(manifest) {
111
103
  const { exec, args, uninstall_args, env, timeouts } = manifest.compose
112
104
  ? composeDaemon(manifest)
@@ -120,6 +112,8 @@ export function daemonConfig(manifest) {
120
112
  ...(uninstall_args ? { uninstall_args } : {}),
121
113
  env,
122
114
  timeouts,
115
+ ...(runtimeBudgetSdkSupported(daemonSdkVersion(manifest))
116
+ ? { operation_seconds: operationBudgetSeconds(manifest) } : {}),
123
117
  ...(manifest.compose ? { network: composeNetwork(manifest) } : {}),
124
118
  }) + "\n");
125
119
  }
@@ -137,11 +131,7 @@ export async function buildDaemonControl(manifest, arch, work, run) {
137
131
  out,
138
132
  "./daemonctl",
139
133
  ], {
140
- cwd: sdkSource(manifest.identityGateway?.sdk ??
141
- (manifest.backend.kind === "go"
142
- ? appSdkVersion(manifest.backend)
143
- : undefined) ??
144
- templateSdkVersion()).goDir,
134
+ cwd: runtimeBudgetSource(sdkSource(daemonSdkVersion(manifest)).goDir, manifest),
145
135
  env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
146
136
  });
147
137
  if (result.missing || result.status !== 0)
@@ -212,3 +202,26 @@ export function copyPayloadTree(root, source, target) {
212
202
  invalid(`Cannot read the daemon payload: ${String(error)}`);
213
203
  }
214
204
  }
205
+ /** Existing lifecycle namespace; a timeout-only declaration runs no daemon. */
206
+ export function readLifecycleTimeouts(value, fail) {
207
+ const v = value ?? {};
208
+ const bad = () => { fail({ code: 'STANDALONE_DAEMON_INVALID', message: 'daemon.timeouts accepts start/stop/uninstall 2–3600s and status 1–3600s.' }); return undefined; };
209
+ if (!record(v) || Object.keys(v).some(k => !['start', 'stop', 'status', 'uninstall'].includes(k)))
210
+ return bad();
211
+ const limits = { start: v.start ?? 30, stop: v.stop ?? 30, status: v.status ?? 5, uninstall: v.uninstall ?? 60 };
212
+ for (const [key, n] of Object.entries(limits))
213
+ if (!Number.isInteger(n) || Number(n) < (key === 'status' ? 1 : 2) || Number(n) > 3600)
214
+ return bad();
215
+ return limits;
216
+ }
217
+ /** Legacy SDKs keep their supported config and timeout bounds. */
218
+ export function runtimeBudgetSource(dir, manifest) {
219
+ if (!existsSync(join(dir, 'internal/runtimebudget/budget.go'))) {
220
+ const timeouts = manifest?.daemon?.timeouts ?? manifest?.lifecycleTimeouts;
221
+ if (!manifest || operationBudgetSeconds(manifest) > 60 || timeouts &&
222
+ (timeouts.start > 120 || timeouts.stop > 120 || timeouts.status > 10 || timeouts.uninstall > 120)) {
223
+ throw new StandaloneBuildError('STANDALONE_DAEMON_BUILD_FAILED', 'The requested timeouts need an SDK with runtime budget support; use SDK 0.17.5 or later before rebuilding.');
224
+ }
225
+ }
226
+ return dir;
227
+ }
@@ -1,3 +1,4 @@
1
+ import { gatewayEntryPath } from './native-gateway.js';
1
2
  import { resolvedSdkLaunch, dsmLauncherType } from "../contract/app-launch.js";
2
3
  import { REOPEN_EVENTS_RUNTIME } from "./reopen-events.js";
3
4
  import { LAUNCH_PARAMS_RUNTIME } from "./launch-params.js";
@@ -120,7 +121,7 @@ export function renderUiConfig(manifest, icons = packageIcons(manifest.icon), lo
120
121
  ...titleFields,
121
122
  icon,
122
123
  ...launcherAccess(manifest),
123
- url: `/webman/3rdparty/${manifest.package}/sso/open.cgi`,
124
+ url: gatewayEntryPath(manifest),
124
125
  },
125
126
  },
126
127
  }, null, 2) + "\n");
@@ -1,3 +1,5 @@
1
+ import { nativeGatewaySupported } from './native-gateway.js';
2
+ import { operationBudgetSeconds, runtimeBudgetSdkSupported } from "../runtime/runtime-budgets.js";
1
3
  import { existsSync, lstatSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem, } from "../build-spk/go-toolchain.js";
@@ -5,7 +7,7 @@ import { goChildEnv } from "../runtime/go-env.js";
5
7
  import { sdkSource } from "../runtime/sdk-source.js";
6
8
  import { checkCgiBinary } from "./binary.js";
7
9
  import { isWebAccessMode, DSM_GROUP_NAME, } from "../contract/web-launch.js";
8
- import { relativeExec, token } from "./daemon.js";
10
+ import { relativeExec, runtimeBudgetSource, token } from "./daemon.js";
9
11
  import { StandaloneBuildError } from "./spk.js";
10
12
  /** The SDK's identity gateway: the daemon wrapper and the DSM-origin authorize CGI. */
11
13
  export const GATEWAY_MAIN = "./idgateway/cmd/gateway";
@@ -309,6 +311,11 @@ export function identityGatewayConfig(m) {
309
311
  const g = m.identityGateway;
310
312
  return (JSON.stringify({
311
313
  package: m.package,
314
+ ...(nativeGatewaySupported(m) ? { native_ingress: true } : {}),
315
+ ...(runtimeBudgetSdkSupported(g.sdk) ? {
316
+ operation_seconds: operationBudgetSeconds(m),
317
+ stop_seconds: m.daemon?.timeouts.stop ?? m.lifecycleTimeouts?.stop ?? 30,
318
+ } : {}),
312
319
  mode: g.mode,
313
320
  access: g.access ?? "dsm-users",
314
321
  ...(g.groups ? { groups: g.groups } : {}),
@@ -385,6 +392,7 @@ export async function buildIdentityGateway(m, arch, work, run) {
385
392
  catch (error) {
386
393
  throw new StandaloneBuildError("SDK_DOWNLOAD_FAILED", error.message.replace(/^\[[A-Z_]+\] /, ""));
387
394
  }
395
+ runtimeBudgetSource(sdk.goDir, m);
388
396
  const out = join(work, `identity-gateway-${arch}`);
389
397
  const ldflags = `-s -w -buildid= -X main.configPath=${packageDest(m)}/${GATEWAY_CONFIG_PATH}`;
390
398
  const result = await run("go", [
@@ -1,3 +1,5 @@
1
+ import { nativePortal } from "./portal.js";
2
+ import { nativeGatewaySupported, renderNativeGatewayConfig, renderNativeGatewayPrepare, NATIVE_GATEWAY_CONFIG, gatewayEntryPath } from './native-gateway.js';
1
3
  import { inspectPortalInfo } from "../portal/info.js";
2
4
  import { isDeepStrictEqual } from "node:util";
3
5
  import { inspectApiDescription } from "./api-description-inspect.js";
@@ -18,7 +20,7 @@ import { familyOfArchToken, NATIVE_ARCH_FAMILIES, } from "../build-spk/dsm-arch.
18
20
  import { CONTAINER_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, PORTAL_MIN_DSM, PORTAL_MIN_DSM_REASON, } from "../build-spk/dsm-version.js";
19
21
  import { ELF_MACHINE } from "./binary.js";
20
22
  import { CGI_PAYLOAD_PATH, entryLauncherAccess } from "./dsm-window.js";
21
- import { daemonConfig, readDaemon } from "./daemon.js";
23
+ import { daemonConfig, readDaemon, readLifecycleTimeouts } from "./daemon.js";
22
24
  import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
23
25
  import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
24
26
  import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
@@ -110,6 +112,10 @@ export async function inspectSpk(spkPath, expectedKind) {
110
112
  (raw.entry !== undefined &&
111
113
  !["admin-only", "app-privilege"].includes(raw.entry)))
112
114
  throw new Error();
115
+ if (raw.lifecycleTimeouts !== undefined && (!readLifecycleTimeouts(raw.lifecycleTimeouts, () => { }) || Object.keys(raw.lifecycleTimeouts).length !== 4))
116
+ throw new Error();
117
+ if (raw.operationSeconds !== undefined && (!Number.isInteger(raw.operationSeconds) || raw.operationSeconds < 1 || raw.operationSeconds > 3600))
118
+ throw new Error();
113
119
  metadata = raw;
114
120
  }
115
121
  catch {
@@ -145,6 +151,7 @@ export async function inspectSpk(spkPath, expectedKind) {
145
151
  (!info.os_min_ver || !isDsmAtLeast(info.os_min_ver, CONTAINER_MIN_DSM)))
146
152
  findings.push("compose identity_gateway requires DSM >= 7.2.1");
147
153
  await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
154
+ const nativeGateway = await recoverNativeGateway(dir, metadata.native_gateway, info, findings);
148
155
  const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
149
156
  // Before any fixed-lifecycle comparison: those see the SDK steps alone.
150
157
  const appScripts = standalone || kind === "container"
@@ -163,7 +170,7 @@ export async function inspectSpk(spkPath, expectedKind) {
163
170
  !isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
164
171
  findings.push(`INFO os_min_ver "${info.os_min_ver}" is below the DSM ${PORTAL_MIN_DSM} a portal needs; ${PORTAL_MIN_DSM_REASON}`);
165
172
  }
166
- checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose);
173
+ checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver);
167
174
  checkScripts(dir, findings);
168
175
  checkUninstall(dir, findings);
169
176
  if (kind === "container")
@@ -191,10 +198,10 @@ export async function inspectSpk(spkPath, expectedKind) {
191
198
  if (standalone && portal)
192
199
  findings.push(...inspectPortalInfo(info, hasExecutor));
193
200
  const daemon = standalone
194
- ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined)
201
+ ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway)
195
202
  : undefined;
196
203
  if (background)
197
- checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go");
204
+ checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts);
198
205
  let executor;
199
206
  if ((metadata.capabilities ?? []).some((name) => name === "rootOps" || name === "delegate" || name === "webapi") ||
200
207
  paths.has("executor/executor")) {
@@ -224,6 +231,7 @@ export async function inspectSpk(spkPath, expectedKind) {
224
231
  if (![...paths].some((path) => path.startsWith("daemon/"))) {
225
232
  const lifecycleManifest = {
226
233
  package: info.package,
234
+ lifecycleTimeouts: metadata.lifecycleTimeouts,
227
235
  capabilities: {
228
236
  rootOps: true,
229
237
  ...(background ? { jobs: { worker: "sha256-v1" } } : {}),
@@ -622,7 +630,8 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
622
630
  os_min_ver: info.os_min_ver,
623
631
  build: Number(info.version?.split("-").pop()),
624
632
  arches: [arch],
625
- backend: { kind: "binary", files: {} },
633
+ backend: { kind: "binary", files: {}, ...(metadata.operationSeconds ? { timeoutSeconds: metadata.operationSeconds } : {}) },
634
+ lifecycleTimeouts: metadata.lifecycleTimeouts,
626
635
  ui: { dir: root, launch: metadata.launch },
627
636
  capabilities: {
628
637
  ...(metadata.capabilities?.includes("composeMounts")
@@ -635,7 +644,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
635
644
  };
636
645
  if (Boolean(c.web) !== Boolean(g))
637
646
  throw new Error("Web ingress must have a complete identity gateway");
638
- if (gateway && gateway !== identityGatewayConfig(manifest))
647
+ if (gateway && !matchesGatewayConfig(gateway, manifest))
639
648
  throw new Error("gateway configuration differs from the declared package contract");
640
649
  if (readOptional(join(dir, "conf/privilege")) !== renderNativePrivilege())
641
650
  throw new Error("Compose hooks must run as the package account");
@@ -648,8 +657,15 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
648
657
  throw new Error("LAUNCHER_TYPE_MISMATCH classless Compose must use the shared url launcher");
649
658
  if (g &&
650
659
  (cfg.package !== info.package ||
651
- info.adminurl !== `/webman/3rdparty/${info.package}/sso/open.cgi`))
660
+ info.adminurl !== gatewayEntryPath(manifest)))
652
661
  throw new Error("gateway entry mismatch");
662
+ for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", {
663
+ composeData: c.plan.app_data,
664
+ portal: Boolean(nativePortal(manifest)),
665
+ }))) {
666
+ if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
667
+ throw new Error(`unexpected WIZARD_UIFILES/${name}; retained-volume guidance must match the plan`);
668
+ }
653
669
  const appScripts = unwrapAppScripts(join(dir, "scripts"), findings);
654
670
  manifest.appScripts = undefined;
655
671
  for (const hook of [
@@ -771,7 +787,7 @@ async function recoverCompose(dir, value, findings) {
771
787
  return undefined;
772
788
  }
773
789
  }
774
- async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined) {
790
+ async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway) {
775
791
  if (![...paths].some((path) => path.startsWith("daemon/")))
776
792
  return;
777
793
  if (info.precheckstartstop !== "yes")
@@ -796,6 +812,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
796
812
  "args",
797
813
  "env",
798
814
  "timeouts",
815
+ "operation_seconds",
799
816
  "uninstall_args",
800
817
  ...(combined ? ["network"] : []),
801
818
  ].includes(key)))
@@ -810,6 +827,8 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
810
827
  }, dir, ["x86_64"], () => { });
811
828
  if (!daemon)
812
829
  throw new Error("invalid config");
830
+ if (config.operation_seconds !== undefined && (!Number.isInteger(config.operation_seconds) || config.operation_seconds < 1 || config.operation_seconds > 3600))
831
+ throw new Error('invalid operation budget');
813
832
  if (combined &&
814
833
  (daemon.exec !== "_sso/gateway" ||
815
834
  JSON.stringify(daemon.args) !== '["run"]' ||
@@ -820,6 +839,9 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
820
839
  throw new Error("missing executable");
821
840
  const manifest = {
822
841
  package: info.package,
842
+ backend: { kind: 'go', dir: dir, main: './cmd/app', timeoutSeconds: config.operation_seconds ?? 60 },
843
+ root: dir,
844
+ ...(combined ? { lifecycleTimeouts: daemon.timeouts } : {}),
823
845
  daemon,
824
846
  ...combined,
825
847
  capabilities: {
@@ -841,11 +863,14 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
841
863
  },
842
864
  }
843
865
  : manifest);
844
- if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== canonical)
866
+ const expectedConfig = JSON.parse(canonical);
867
+ if (config.operation_seconds === undefined)
868
+ delete expectedConfig.operation_seconds;
869
+ if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== JSON.stringify(expectedConfig) + '\n')
845
870
  throw new Error("non-canonical daemon config");
846
871
  for (const hook of ["start-stop-status", "preupgrade", "preuninst"]) {
847
872
  if (readFileSync(join(dir, "scripts", hook), "utf8") !==
848
- renderStandaloneScript(hook, manifest))
873
+ renderStandaloneScript(hook, nativeGateway ? { ...manifest, os_min_ver: info.os_min_ver, identityGateway: nativeGateway } : manifest))
849
874
  findings.push(`daemon scripts/${hook} does not match the fixed lifecycle`);
850
875
  }
851
876
  return daemon;
@@ -917,12 +942,50 @@ async function checkContainerExposure(dir, packageTgz, findings) {
917
942
  function readOptional(path) {
918
943
  return existsSync(path) ? readFileSync(path, "utf8") : undefined;
919
944
  }
920
- function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose) {
945
+ async function recoverNativeGateway(dir, value, info, findings) {
946
+ if (value === undefined)
947
+ return undefined;
948
+ try {
949
+ if (!value || typeof value !== 'object' || Array.isArray(value))
950
+ throw new Error('invalid native gateway record');
951
+ const plan = value;
952
+ if (typeof plan.sdk !== 'string' || !plan.listen || typeof plan.listen !== 'object' || Array.isArray(plan.listen)
953
+ || !Object.keys(plan.listen).length || Object.keys(plan).some(k => !['sdk', 'listen'].includes(k))
954
+ || Object.entries(plan.listen).some(([k, v]) => !['http', 'https'].includes(k) || !Number.isInteger(v) || v < 1 || v > 65535))
955
+ throw new Error('invalid native gateway ports');
956
+ const target = { package: info.package, os_min_ver: info.os_min_ver, identityGateway: plan };
957
+ if (!nativeGatewaySupported(target))
958
+ throw new Error('unsupported native gateway SDK/DSM floor');
959
+ const root = join(dir, 'native-gateway-inspection');
960
+ mkdirSync(root);
961
+ await tar.x({ file: join(dir, 'package.tgz'), cwd: root, filter: path => ['sso/gateway.json', NATIVE_GATEWAY_CONFIG].includes(path.replace(/^\.\//, '')) });
962
+ const config = JSON.parse(readFileSync(join(root, 'sso/gateway.json'), 'utf8'));
963
+ if (config.native_ingress !== true || config.package !== info.package || !isDeepStrictEqual(config.listen, plan.listen)
964
+ || config.state_dir !== `/var/packages/${info.package}/var/sso`)
965
+ throw new Error('native gateway config does not match its package');
966
+ if (readOptional(join(root, NATIVE_GATEWAY_CONFIG)) !== renderNativeGatewayConfig(target))
967
+ throw new Error('native gateway ingress differs from its fixed proxy');
968
+ for (const key of ['instuninst_restart_services', 'startstop_restart_services'])
969
+ if (!(info[key] ?? '').split(/\s+/).includes('nginx.service'))
970
+ throw new Error(`native gateway requires INFO ${key}`);
971
+ for (const hook of ['postinst', 'postupgrade'])
972
+ if (!readFileSync(join(dir, 'scripts', hook), 'utf8').includes(renderNativeGatewayPrepare(target)))
973
+ throw new Error('native gateway credential preparation is missing');
974
+ return plan;
975
+ }
976
+ catch (e) {
977
+ findings.push(`NATIVE_GATEWAY_INVALID ${e.message}`);
978
+ return undefined;
979
+ }
980
+ }
981
+ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose, nativeGateway, osMinVer) {
921
982
  const confDir = join(dir, "conf");
922
983
  const entries = existsSync(confDir) ? readdirSync(confDir) : [];
923
984
  const nativeResource = kind !== "container"
924
985
  ? renderStandaloneResource({
925
986
  package: pkg ?? "",
987
+ os_min_ver: osMinVer,
988
+ ...(nativeGateway ? { identityGateway: nativeGateway } : {}),
926
989
  ...(compose ? { compose } : {}),
927
990
  capabilities: background
928
991
  ? { jobs: { worker: "sha256-v1" } }
@@ -979,7 +1042,7 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, c
979
1042
  }
980
1043
  }
981
1044
  /** The opt-in package user unit must be exactly what the generator writes. */
982
- function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false) {
1045
+ function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false, lifecycleTimeouts) {
983
1046
  const pkg = info.package;
984
1047
  if (!pkg)
985
1048
  return;
@@ -997,12 +1060,13 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
997
1060
  if (units.length !== 1 ||
998
1061
  units[0] !== unit ||
999
1062
  read(`${BACKGROUND_UNIT_DIR}/${unit}`) !==
1000
- renderBackgroundUnit(pkg, bridge ? BRIDGE_WORKER_ARGS : [])) {
1063
+ renderBackgroundUnit(pkg, bridge ? BRIDGE_WORKER_ARGS : [], daemon?.timeouts.stop ?? lifecycleTimeouts?.stop ?? 30)) {
1001
1064
  findings.push(`${BACKGROUND_UNIT_DIR} must contain only the fixed ${unit} (runs as the package account, no User=, no root)`);
1002
1065
  }
1003
1066
  if (read("scripts/start-stop-status") !==
1004
1067
  renderStandaloneScript("start-stop-status", {
1005
1068
  package: pkg,
1069
+ lifecycleTimeouts,
1006
1070
  ...(daemon ? { daemon } : {}),
1007
1071
  capabilities: {
1008
1072
  jobs: { worker: "sha256-v1" },
@@ -1149,7 +1213,7 @@ function checkScripts(dir, findings) {
1149
1213
  }
1150
1214
  const DATA_KEYS = ["pkgwizard_remove_additional", "delete_app_data"];
1151
1215
  const DELETE_COMMAND = /\brm\s+-[a-zA-Z]*[rR]|\bfind\b[^\n]*(-delete|-exec\s+rm)/;
1152
- const UPGRADE_GUARD = /SYNOPKG_PKG_STATUS[^\n]*!=\s*"?UPGRADE"?/;
1216
+ const UPGRADE_GUARD = /SYNOPKG_PKG_STATUS[^\n]*(?:!=\s*"?UPGRADE"?|=\s*"?UNINSTALL"?)/;
1153
1217
  function uninstallBoxes(file, keys) {
1154
1218
  try {
1155
1219
  const steps = JSON.parse(readFileSync(file, "utf8"));
@@ -1206,7 +1270,7 @@ function checkUninstall(dir, findings) {
1206
1270
  const deletes = DELETE_COMMAND.test(body);
1207
1271
  if (deletes) {
1208
1272
  if (!UPGRADE_GUARD.test(body))
1209
- findings.push('UNINSTALL_NO_UPGRADE_GUARD scripts/postuninst deletes data without the [ "$SYNOPKG_PKG_STATUS" != UPGRADE ] guard, so an upgrade deletes it by mistake');
1273
+ findings.push('UNINSTALL_NO_UPGRADE_GUARD scripts/postuninst deletes data without an UNINSTALL-only or != UPGRADE guard, so an upgrade deletes it by mistake');
1210
1274
  if (!/is_package_area\(\)\s*\{/.test(body) ||
1211
1275
  !/if ! is_package_area "\$real"/.test(body))
1212
1276
  findings.push("UNINSTALL_UNSAFE_DELETE scripts/postuninst does not verify the deleted paths are under /volumeN/@appdata|@apphome|@appconf|@appstore/<package>; an empty or redirected variable deletes the wrong data");
@@ -1265,3 +1329,11 @@ export async function listPayload(packageTgz, findings) {
1265
1329
  });
1266
1330
  return payload;
1267
1331
  }
1332
+ function matchesGatewayConfig(actual, manifest) {
1333
+ const saved = JSON.parse(actual), expected = JSON.parse(identityGatewayConfig(manifest));
1334
+ // Older artifacts omit both work-budget members; do not reinterpret their bytes.
1335
+ for (const key of ['operation_seconds', 'stop_seconds'])
1336
+ if (saved[key] === undefined)
1337
+ delete expected[key];
1338
+ return actual === JSON.stringify(expected) + '\n';
1339
+ }
@@ -1,3 +1,6 @@
1
+ import { renderComposeRetainedData } from "../build-spk/compose-retained-data.js";
2
+ import { renderNativeGatewayPrepare } from './native-gateway.js';
3
+ import { renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
1
4
  import { composeDaemon, renderComposeInstall } from "./compose.js";
2
5
  import { renderBackgroundControl, renderBackgroundWorkerStart, renderBackgroundWorkerStop, renderUserManagerRecovery, backgroundUnitName, } from "./background.js";
3
6
  import { renderPackagePostuninst } from "../build-spk/native-lifecycle.js";
@@ -10,6 +13,14 @@ import { composeAppScript } from "./app-scripts.js";
10
13
  const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_uninstall_hook\n';
11
14
  /** Package-account lifecycle hooks for CGI, daemon and background packages. */
12
15
  export function renderStandaloneScript(name, manifest) {
16
+ const script = renderStandaloneScriptInternal(name, manifest);
17
+ const prepared = ['postinst', 'postupgrade'].includes(name) ? renderNativeGatewayPrepare(manifest) : '';
18
+ const sdk = prepared ? script.replace('#!/bin/sh\n', '#!/bin/sh\n' + prepared) : script;
19
+ return manifest.appScripts?.some((app) => app === name)
20
+ ? composeAppScript(name, sdk)
21
+ : sdk;
22
+ }
23
+ function renderStandaloneScriptInternal(name, manifest) {
13
24
  if (manifest.compose) {
14
25
  const native = {
15
26
  ...manifest,
@@ -18,7 +29,7 @@ export function renderStandaloneScript(name, manifest) {
18
29
  };
19
30
  const body = name === "preinst"
20
31
  ? renderComposeInstall(manifest.package)
21
- : renderStandaloneScript(name, { ...native, appScripts: undefined });
32
+ : renderStandaloneScriptInternal(name, { ...native, appScripts: undefined });
22
33
  const control = `"/var/packages/${manifest.package}/target/daemon/control"`;
23
34
  const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${manifest.package}/var}"`;
24
35
  const preflight = name === "start-stop-status"
@@ -29,7 +40,14 @@ export function renderStandaloneScript(name, manifest) {
29
40
  !manifest.compose.plan.mounts_blocked
30
41
  ? `"/var/packages/${manifest.package}/target/compose/control" remember-ingress --project "/var/packages/${manifest.package}/target/compose" --package '${manifest.package}' || exit $?\n`
31
42
  : "";
32
- const ledger = preflight + remember +
43
+ const project = `\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/compose`;
44
+ const diagnosis = ["postinst", "postupgrade"].includes(name)
45
+ ? renderComposeDiagnosis(project, name)
46
+ : name === "start-stop-status"
47
+ ? `if [ "$1" = prestart ]; then\n ${renderComposeDiagnosis(project, "prestart")}fi\n`
48
+ : "";
49
+ const ledger = diagnosis + preflight + remember +
50
+ (name === "postuninst" ? renderComposeRetainedData(manifest.compose.plan.app_data) : "") +
33
51
  (manifest.identityGateway && !manifest.compose.plan.mounts_blocked
34
52
  ? name === "start-stop-status"
35
53
  ? `if [ "$1" = restart ]; then\n "$0" stop || exit $?\n exec "$0" start\nfi\nif [ "$1" = start ]; then\n ${control} acquire ${paths} || exit $?\nelif [ "$1" = stop ]; then\n ${control} release ${paths} || exit $?\nfi\n`
@@ -40,18 +58,14 @@ export function renderStandaloneScript(name, manifest) {
40
58
  const withLedger = ledger
41
59
  ? body.replace("#!/bin/sh\n", "#!/bin/sh\n" + ledger)
42
60
  : body;
43
- return manifest.appScripts?.some((app) => app === name)
44
- ? composeAppScript(name, withLedger)
45
- : withLedger;
61
+ return withLedger;
46
62
  }
47
63
  const portal = nativePortal(manifest);
48
64
  const script = renderCapabilityScript(name, manifest);
49
65
  const sdk = portal
50
66
  ? script.replace("#!/bin/sh\n", () => `#!/bin/sh\n${renderPortalLifecycle(name, portal)}`)
51
67
  : script;
52
- return manifest.appScripts?.some((app) => app === name)
53
- ? composeAppScript(name, sdk)
54
- : sdk;
68
+ return sdk;
55
69
  }
56
70
  function renderCapabilityScript(name, manifest) {
57
71
  if (name === "preupgrade" &&
@@ -152,7 +166,7 @@ function renderStandaloneScriptBody(name, manifest) {
152
166
  if (manifest.daemon)
153
167
  return renderDaemonScript(name, manifest);
154
168
  if (manifest.capabilities.jobs && name === "start-stop-status")
155
- return renderBackgroundControl(manifest.package);
169
+ return renderBackgroundControl(manifest.package, manifest.lifecycleTimeouts?.start ?? 30);
156
170
  if (name === "start-stop-status") {
157
171
  return `#!/bin/sh
158
172
  case "$1" in
@@ -184,7 +198,7 @@ function renderDelegatedDaemonScript(name, manifest) {
184
198
  return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}if [ "\${SYNOPKG_PKG_STATUS:-}" = UPGRADE ]; then\n${stop("stop", "stop")}fi\n${purge}`;
185
199
  }
186
200
  return `#!/bin/sh
187
- ${workerStop}${renderUserManagerRecovery()}${renderBackgroundWorkerStart(pkg)}case "$1" in
201
+ ${workerStop}${renderUserManagerRecovery(manifest.daemon?.timeouts?.start ?? manifest.lifecycleTimeouts?.start ?? 30)}${renderBackgroundWorkerStart(pkg)}case "$1" in
188
202
  prestart) recover_user_manager ;;
189
203
  prestop) exit 0 ;;
190
204
  start)
@@ -86,6 +86,7 @@ export interface StandaloneManifest {
86
86
  tasks: ExecutorTask[];
87
87
  };
88
88
  daemon?: Daemon;
89
+ lifecycleTimeouts?: Daemon["timeouts"];
89
90
  /** `identity_gateway`: signs DSM users in to the daemon's own web UI. */
90
91
  identityGateway?: IdentityGateway;
91
92
  compose?: ComposeApp;
@@ -9,7 +9,7 @@ import { isNativeArchFamily, } from "../build-spk/dsm-arch.js";
9
9
  import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
10
10
  import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
11
11
  import { readComposeApp } from "./compose.js";
12
- import { readDaemon } from "./daemon.js";
12
+ import { readDaemon, readLifecycleTimeouts } from "./daemon.js";
13
13
  import { readIdentityGateway, } from "./identity-gateway.js";
14
14
  import { readUninstall } from "./uninstall.js";
15
15
  import { readAppScripts } from "./app-scripts.js";
@@ -231,8 +231,10 @@ function readManifest(raw, c) {
231
231
  c.fail("STANDALONE_API_PERMISSIONS_INVALID", `backend/${API_PERMISSIONS_FILE[backend.kind]} must define the centralized API policy: ${String(error)}`);
232
232
  }
233
233
  }
234
+ const timeoutOnly = isRecord(raw.daemon) && Object.keys(raw.daemon).length === 1 && 'timeouts' in raw.daemon;
235
+ const lifecycleTimeouts = timeoutOnly ? readLifecycleTimeouts(raw.daemon.timeouts, (e) => c.fail(e.code, e.message)) : undefined;
234
236
  const daemon = arches &&
235
- readDaemon(raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
237
+ readDaemon(timeoutOnly ? undefined : raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
236
238
  const gatewayRaw = isRecord(raw.identity_gateway) ? raw.identity_gateway : {};
237
239
  if (raw.compose !== undefined && isRecord(raw.ctl) && raw.ctl.enabled === true)
238
240
  c.fail("CTL_UNSUPPORTED_BACKEND", "Compose does not support automatic ctl descriptions; disable ctl.");
@@ -246,7 +248,7 @@ function readManifest(raw, c) {
246
248
  composeMounts: capabilities.composeMounts,
247
249
  });
248
250
  if (raw.compose !== undefined &&
249
- (raw.daemon !== undefined ||
251
+ ((raw.daemon !== undefined && !timeoutOnly) ||
250
252
  Object.keys(capabilities).some((k) => k !== "composeMounts") ||
251
253
  !osMinVer ||
252
254
  !isDsmAtLeast(osMinVer, "7.2.1-69057")))
@@ -280,7 +282,7 @@ function readManifest(raw, c) {
280
282
  s.port === identityGateway.oidc?.issuer_port))))
281
283
  c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose service ports must contain upstream and not overlap gateway or issuer ports.");
282
284
  if (capabilities.jobs &&
283
- raw.daemon !== undefined &&
285
+ raw.daemon !== undefined && !timeoutOnly &&
284
286
  capabilities.delegate?.lifetime !== "until-revoked") {
285
287
  c.fail("CAPABILITY_COMBINATION_INVALID", "daemon can only coexist with an explicit until-revoked delegate fixed worker; plain capabilities.jobs stays mutually exclusive.");
286
288
  }
@@ -299,7 +301,7 @@ function readManifest(raw, c) {
299
301
  delegate.destinations?.length)) {
300
302
  c.fail("CAPABILITY_UNSUPPORTED", `For backend.runtime: ${backend.kind}, capabilities.delegate supports only operations: [files.moveFromStaging], destinations and lifetime: until-revoked.`);
301
303
  }
302
- if (raw.daemon !== undefined)
304
+ if (raw.daemon !== undefined && !timeoutOnly)
303
305
  c.fail("CAPABILITY_UNSUPPORTED", `backend.runtime: ${backend.kind} does not support daemon; a v1 App runs once per request (exec mode).`);
304
306
  if (permissions)
305
307
  checkBridgeMethods(backend, permissions, c);
@@ -438,6 +440,7 @@ function readManifest(raw, c) {
438
440
  ...(rootOps ? { rootOps } : {}),
439
441
  ...(rootPrograms ? { rootPrograms } : {}),
440
442
  ...(daemon ? { daemon } : {}),
443
+ ...(lifecycleTimeouts ? { lifecycleTimeouts } : {}),
441
444
  ...(identityGateway ? { identityGateway } : {}),
442
445
  ...(compose ? { compose } : {}),
443
446
  ...(uninstall ? { uninstall } : {}),
@@ -0,0 +1,17 @@
1
+ import type { IdentityGateway } from './identity-gateway.js';
2
+ export type NativeGatewayPlan = Pick<IdentityGateway, 'sdk' | 'listen'>;
3
+ type Target = {
4
+ package: string;
5
+ os_min_ver?: string;
6
+ identityGateway?: NativeGatewayPlan;
7
+ };
8
+ export declare const NATIVE_GATEWAY_CONFIG = "sso/native-nginx.conf";
9
+ export declare function nativeGatewaySdk(sdk: string): boolean;
10
+ export declare function nativeGatewaySupported(m: Target): boolean;
11
+ export declare function gatewayEntryPath(m: Target): string;
12
+ export declare function legacyGatewayNotice(): string;
13
+ export declare function nativeGatewayResource(m: Target): Record<string, unknown> | undefined;
14
+ export declare function renderNativeGatewayConfig(m: Target): string;
15
+ /** Runs after DSM creates pkgvar, before package start activates the nginx resource. */
16
+ export declare function renderNativeGatewayPrepare(m: Target): string;
17
+ export {};