@synopackageland/cli 0.17.4 → 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.
- package/CHANGELOG.md +15 -1
- package/README.md +27 -1
- package/dist/build-spk/compose-retained-data.d.ts +11 -0
- package/dist/build-spk/compose-retained-data.js +34 -0
- package/dist/build-spk/delete-app-data.d.ts +1 -1
- package/dist/build-spk/delete-app-data.js +1 -1
- package/dist/build-spk/uninstall-wizard.d.ts +2 -0
- package/dist/build-spk/uninstall-wizard.js +2 -1
- package/dist/build-spk/wizard-messages.d.ts +22 -0
- package/dist/build-spk/wizard-messages.js +22 -0
- package/dist/ctl/cli.js +10 -6
- package/dist/ctl/flows.js +1 -1
- package/dist/ctl/service.d.ts +4 -1
- package/dist/ctl/service.js +20 -6
- package/dist/ctl/transfers.d.ts +1 -1
- package/dist/ctl/transfers.js +5 -2
- package/dist/ctl/transport.d.ts +2 -1
- package/dist/ctl/transport.js +7 -3
- package/dist/runtime/compose-data.d.ts +10 -0
- package/dist/runtime/compose-data.js +20 -0
- package/dist/runtime/executor-digests.json +11 -0
- package/dist/runtime/executor-policy.d.ts +25 -0
- package/dist/runtime/executor-policy.js +10 -1
- package/dist/runtime/runtime-budgets.d.ts +24 -0
- package/dist/runtime/runtime-budgets.js +13 -0
- package/dist/standalone/background.d.ts +3 -3
- package/dist/standalone/background.js +12 -11
- package/dist/standalone/compose.d.ts +3 -1
- package/dist/standalone/compose.js +8 -1
- package/dist/standalone/daemon.d.ts +4 -0
- package/dist/standalone/daemon.js +36 -23
- package/dist/standalone/dsm-window.js +2 -1
- package/dist/standalone/identity-gateway.js +9 -1
- package/dist/standalone/inspect-spk.js +87 -15
- package/dist/standalone/lifecycle.js +16 -9
- package/dist/standalone/manifest.d.ts +1 -0
- package/dist/standalone/manifest.js +8 -5
- package/dist/standalone/native-gateway.d.ts +17 -0
- package/dist/standalone/native-gateway.js +115 -0
- package/dist/standalone/resource.d.ts +3 -0
- package/dist/standalone/resource.js +3 -1
- package/dist/standalone/spk.js +14 -3
- package/dist/standalone/uninstall-hook.js +21 -8
- package/dist/standalone/uninstall.d.ts +1 -1
- package/dist/standalone/uninstall.js +2 -2
- package/package.json +2 -2
- package/skills/reference/compose.md +31 -0
- package/skills/reference/create.md +1 -1
- package/skills/reference/lifecycle.md +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.17.5 (2026-10-20)
|
|
4
|
+
|
|
5
|
+
- Run native HTTPS preparation inside the SDK lifecycle steps before App hooks, keeping generated hook wrappers inspectable.
|
|
6
|
+
|
|
7
|
+
- Preserve older SDK gateway/controller JSON and supported timeout limits so their direct-connection fallback remains buildable; reject expanded budgets that require SDK 0.17.5.
|
|
8
|
+
|
|
9
|
+
- Keep Compose named volumes without adding executor/root permissions; warn before uninstall and report their exact undeleted names in nine native locales when delete-data is selected. Clear package-owned data/config/home contents only on explicit uninstall, preserving data on upgrades, stops and keep.
|
|
10
|
+
|
|
11
|
+
- Default supported gateway Apps to a DSM-managed HTTPS origin using native package resources and a private authenticated gateway socket. Preserve direct connections for older SDK/DSM targets with an explanation before opening them.
|
|
12
|
+
- Prepare private connection credentials as the package account after DSM creates package state, and reject modified gateway proxy settings during SPK inspection.
|
|
13
|
+
- Apply `ctl --timeout` to discovery, JSON requests, polling and transfer inactivity without replaying uncertain writes; use a 62-second default transport allowance.
|
|
14
|
+
- Reuse `daemon.timeouts` for native, Compose and background lifecycle work, including declarations that need no daemon executable. Allow lifecycle and App purge budgets up to 3600 seconds while retaining the Compose startup cap.
|
|
15
|
+
- Generate matching gateway, supervisor and approved executor budgets, preserve them through SPK inspection, and report pending uninstall cleanup within bounded TERM/reap reserve.
|
|
16
|
+
|
|
17
|
+
- Pin templates 0.17.8 and SDK/App 0.17.5, including UI 0.12.4 full-page layout. Updated executor and lifecycle payloads require administrator reapproval after upgrading rebuilt Apps.
|
|
4
18
|
|
|
5
19
|
## 0.17.4 (2026-10-19)
|
|
6
20
|
|
package/README.md
CHANGED
|
@@ -315,7 +315,11 @@ App is absent from discovery results; no business operation runs.
|
|
|
315
315
|
`ctl host list/show` can also succeed while reporting `authenticated:false` and
|
|
316
316
|
exit 0. Check discovery `apps`/`diagnostics` and session fields as well as the exit
|
|
317
317
|
code when deciding whether an App is ready.
|
|
318
|
-
|
|
318
|
+
`ctl --timeout <seconds>` applies to discovery, JSON calls, polling and transfer
|
|
319
|
+
inactivity. The transport default is 62 seconds; a supplied 1–3600-second budget
|
|
320
|
+
is shared across the operation. Jobs retain their described wait default when no
|
|
321
|
+
override is supplied. After a network or job timeout, query the App's state before
|
|
322
|
+
retrying a write.
|
|
319
323
|
|
|
320
324
|
Read `synopkgland skill show ctl` for the AI workflow, batch discovery approval,
|
|
321
325
|
cache refresh, host removal/revocation and explicit `ctl.commands` authoring.
|
|
@@ -328,3 +332,25 @@ share a request; results keep input order and retain per-method errors. A batch
|
|
|
328
332
|
uses the same token recovery, request deadline and cancellation as `ctx.call`,
|
|
329
333
|
and is never replayed automatically. Older SDKs and binary backends retain the
|
|
330
334
|
single-call client. Rebuild the window with the current CLI/SDK to adopt it.
|
|
335
|
+
|
|
336
|
+
### DSM-managed gateway HTTPS
|
|
337
|
+
|
|
338
|
+
With gateway SDK 0.17.5 or later and a package target of DSM 7.2-64570 or later,
|
|
339
|
+
the build uses DSM's native web-config worker for a separate HTTPS App origin.
|
|
340
|
+
No Web Station dependency or executor capability is added. nginx inherits DSM's
|
|
341
|
+
system default certificate, which must be trusted by the browser and cover the
|
|
342
|
+
App hostname. The package never reads DSM's private key.
|
|
343
|
+
|
|
344
|
+
The declared HTTPS port is served by nginx. If only an HTTP port was declared,
|
|
345
|
+
that port becomes the HTTPS entry; use the generated gateway launcher. A second
|
|
346
|
+
HTTP port redirects to HTTPS. nginx connects to a private, mutually authenticated
|
|
347
|
+
gateway socket and preserves the session, client IP, CSRF and WebSocket checks.
|
|
348
|
+
The package-account post-install/upgrade hooks prepare private connection
|
|
349
|
+
credentials with DSM's OpenSSL after DSM creates package state and before
|
|
350
|
+
package start activates nginx.
|
|
351
|
+
|
|
352
|
+
Older SDK/DSM targets retain the direct listener and explain its certificate or
|
|
353
|
+
unencrypted-HTTP limitations before opening it. Native resource/startup failures
|
|
354
|
+
fail closed; they do not silently expose a direct listener. The native runtime
|
|
355
|
+
requires the corresponding SDK release; this CLI change does not publish that
|
|
356
|
+
release or change existing manifest version pins.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ComposeAppData } from "../runtime/compose-data.js";
|
|
2
|
+
import { type Locale } from "../standalone/i18n.js";
|
|
3
|
+
export declare const COMPOSE_DATA_RETAINED = "COMPOSE_DATA_RETAINED";
|
|
4
|
+
/** Same segment spacing as the SDK image-pull diagnosis; support code goes last. */
|
|
5
|
+
export declare function composeRetainedMessage(data: ComposeAppData, locale: Locale): string;
|
|
6
|
+
/**
|
|
7
|
+
* Embedded in the native hook: postuninst need not find an already removed
|
|
8
|
+
* payload/helper. Names describe the plan, not a privileged Docker readback.
|
|
9
|
+
* Reporting is informational and must never prevent package removal.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderComposeRetainedData(data?: ComposeAppData): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { COMPOSE_DELETE_CHOICE } from "../runtime/compose-data.js";
|
|
2
|
+
import { DSM_LANGS, LOCALES } from "../standalone/i18n.js";
|
|
3
|
+
import { translator } from "./wizard-i18n.js";
|
|
4
|
+
export const COMPOSE_DATA_RETAINED = "COMPOSE_DATA_RETAINED";
|
|
5
|
+
/** Same segment spacing as the SDK image-pull diagnosis; support code goes last. */
|
|
6
|
+
export function composeRetainedMessage(data, locale) {
|
|
7
|
+
const t = translator(locale);
|
|
8
|
+
const separator = ["zh-TW", "zh-CN", "ja"].includes(locale) ? "" : " ";
|
|
9
|
+
return t("compose.retained.message", { volumes: data.volumes.map(v => v.name).join(", ") }) +
|
|
10
|
+
separator + t("compose.diagnosis.error_code", { code: COMPOSE_DATA_RETAINED });
|
|
11
|
+
}
|
|
12
|
+
const quote = (text) => `'${text.replaceAll("'", "'\\''")}'`;
|
|
13
|
+
/**
|
|
14
|
+
* Embedded in the native hook: postuninst need not find an already removed
|
|
15
|
+
* payload/helper. Names describe the plan, not a privileged Docker readback.
|
|
16
|
+
* Reporting is informational and must never prevent package removal.
|
|
17
|
+
*/
|
|
18
|
+
export function renderComposeRetainedData(data) {
|
|
19
|
+
if (!data?.volumes.length)
|
|
20
|
+
return "";
|
|
21
|
+
const cases = LOCALES.map(locale => ` ${locale}|${DSM_LANGS[locale]}) compose_data_message=${quote(composeRetainedMessage(data, locale))} ;;`).join("\n");
|
|
22
|
+
return `${COMPOSE_DELETE_CHOICE}if [ "$compose_remove_data" -eq 1 ]; then
|
|
23
|
+
case "\${SYNOPKG_DSM_LANGUAGE:-enu}" in
|
|
24
|
+
${cases}
|
|
25
|
+
*) compose_data_message=${quote(composeRetainedMessage(data, "en"))} ;;
|
|
26
|
+
esac
|
|
27
|
+
if [ -n "\${SYNOPKG_TEMP_LOGFILE:-}" ] && [ ! -L "$SYNOPKG_TEMP_LOGFILE" ]; then
|
|
28
|
+
if [ ! -e "$SYNOPKG_TEMP_LOGFILE" ] || [ -f "$SYNOPKG_TEMP_LOGFILE" ]; then
|
|
29
|
+
(printf '%s\\n' "$compose_data_message" >> "$SYNOPKG_TEMP_LOGFILE") 2>/dev/null || :
|
|
30
|
+
fi
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* pre-0.5 key `delete_app_data` and DSM's `wizard_` mirror of either key are
|
|
9
9
|
* still honoured for scripted callers.
|
|
10
10
|
*
|
|
11
|
-
* Deletion
|
|
11
|
+
* Deletion runs only on exact UNINSTALL, never on an upgrade or stop: DSM replays postuninst for the old
|
|
12
12
|
* version, and the new version must find its data.
|
|
13
13
|
*
|
|
14
14
|
* Every root is resolved first and must land on `/volumeN/@<area>/<package>`
|
|
@@ -4,7 +4,7 @@ function shellQuote(value) {
|
|
|
4
4
|
export function renderDeleteAppDataBlock(packageName, roots, options = {}) {
|
|
5
5
|
const calls = roots.map((root) => ` delete_root "$${root.variable}" ${root.area}`).join("\n");
|
|
6
6
|
return `remove_additional=0
|
|
7
|
-
if [ "\${SYNOPKG_PKG_STATUS:-}"
|
|
7
|
+
if [ "\${SYNOPKG_PKG_STATUS:-}" = UNINSTALL ]; then
|
|
8
8
|
for flag in "\${pkgwizard_remove_additional:-}" "\${wizard_pkgwizard_remove_additional:-}" "\${delete_app_data:-}" "\${wizard_delete_app_data:-}"; do
|
|
9
9
|
case "$flag" in
|
|
10
10
|
true|True|TRUE|yes|1) remove_additional=1 ;;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ComposeAppData } from "../runtime/compose-data.js";
|
|
1
2
|
import type { Locale } from "../standalone/i18n.js";
|
|
2
3
|
/** The key DSM's built-in packages use for their "also remove my data" box. */
|
|
3
4
|
export declare const REMOVE_ADDITIONAL_KEY = "pkgwizard_remove_additional";
|
|
@@ -5,6 +6,7 @@ export declare const REMOVE_ADDITIONAL_KEY = "pkgwizard_remove_additional";
|
|
|
5
6
|
export declare const DELETE_STATE_KEY = "pkgwizard_delete_state";
|
|
6
7
|
export type UninstallWizardKind = "native" | "container";
|
|
7
8
|
interface UninstallWizardOptions {
|
|
9
|
+
composeData?: ComposeAppData;
|
|
8
10
|
/** The App runs the root executor (rootOps or delegate), which purges DSM's leftovers on uninstall. */
|
|
9
11
|
executor?: boolean;
|
|
10
12
|
/** The App declares a Login Portal alias (web-config app-portal); DSM reloads nginx through INFO. */
|
|
@@ -8,7 +8,7 @@ function list(items) {
|
|
|
8
8
|
return `<ul class="synopkg-ul-list">${items.map((item) => `<li>${item}</li>`).join("")}</ul>`;
|
|
9
9
|
}
|
|
10
10
|
const html = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
11
|
-
function renderUninstallWizard(t, locale, packageName, kind, { executor = false, portal = false, portalEntry = false, state, rootPurge = false }) {
|
|
11
|
+
function renderUninstallWizard(t, locale, packageName, kind, { executor = false, portal = false, portalEntry = false, state, rootPurge = false, composeData }) {
|
|
12
12
|
const vars = { package: packageName };
|
|
13
13
|
return [{
|
|
14
14
|
step_title: t("uninstall.step"),
|
|
@@ -33,6 +33,7 @@ function renderUninstallWizard(t, locale, packageName, kind, { executor = false,
|
|
|
33
33
|
] : []),
|
|
34
34
|
...(executor && rootPurge ? [{ desc: t("uninstall.purge.root", vars) }] : []),
|
|
35
35
|
...(state ? [{ desc: t("uninstall.state.heading") }, { desc: list(state.items(locale).map(html)) }] : []),
|
|
36
|
+
...(composeData?.volumes.length ? [{ desc: t("uninstall.composeRetained", { volumes: composeData.volumes.map(v => v.name).join(", ") }) }] : []),
|
|
36
37
|
{ desc: t("uninstall.backup") },
|
|
37
38
|
{
|
|
38
39
|
type: "multiselect",
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
export declare const WIZARD_MESSAGES: {
|
|
2
|
+
"uninstall.composeRetained": {
|
|
3
|
+
en: string;
|
|
4
|
+
"zh-TW": string;
|
|
5
|
+
"zh-CN": string;
|
|
6
|
+
ja: string;
|
|
7
|
+
ko: string;
|
|
8
|
+
de: string;
|
|
9
|
+
fr: string;
|
|
10
|
+
es: string;
|
|
11
|
+
it: string;
|
|
12
|
+
};
|
|
13
|
+
"compose.retained.message": {
|
|
14
|
+
en: string;
|
|
15
|
+
"zh-TW": string;
|
|
16
|
+
"zh-CN": string;
|
|
17
|
+
ja: string;
|
|
18
|
+
ko: string;
|
|
19
|
+
de: string;
|
|
20
|
+
fr: string;
|
|
21
|
+
es: string;
|
|
22
|
+
it: string;
|
|
23
|
+
};
|
|
2
24
|
"compose.diagnosis.registry_check": {
|
|
3
25
|
en: string;
|
|
4
26
|
"zh-TW": string;
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
export const WIZARD_MESSAGES = {
|
|
2
|
+
"uninstall.composeRetained": {
|
|
3
|
+
"en": "Container data volumes are kept, even when you select delete-data: {volumes}. Their data will NOT be deleted.",
|
|
4
|
+
"zh-TW": "即使勾選刪除資料,容器資料磁碟區會保留:{volumes}。其中的資料不會刪除。",
|
|
5
|
+
"zh-CN": "即使勾选删除数据,容器数据卷也会保留:{volumes}。其中的数据不会删除。",
|
|
6
|
+
"ja": "データ削除を選択しても、コンテナのデータボリュームは保持されます:{volumes}。その中のデータは削除されません。",
|
|
7
|
+
"ko": "데이터 삭제를 선택해도 컨테이너 데이터 볼륨은 보존됩니다: {volumes}. 볼륨의 데이터는 삭제되지 않습니다.",
|
|
8
|
+
"de": "Container-Datenvolumes bleiben erhalten, auch wenn Sie die Datenlöschung auswählen: {volumes}. Die darin gespeicherten Daten werden NICHT gelöscht.",
|
|
9
|
+
"fr": "Les volumes de données des conteneurs sont conservés, même si vous choisissez de supprimer les données : {volumes}. Leurs données ne seront PAS supprimées.",
|
|
10
|
+
"es": "Los volúmenes de datos de los contenedores se conservan aunque seleccione eliminar datos: {volumes}. Sus datos NO se eliminarán.",
|
|
11
|
+
"it": "I volumi dati dei container vengono conservati anche se si seleziona l’eliminazione dei dati: {volumes}. I dati al loro interno NON verranno eliminati."
|
|
12
|
+
},
|
|
13
|
+
"compose.retained.message": {
|
|
14
|
+
"en": "Container data volumes were NOT deleted: {volumes}. Their data is kept.",
|
|
15
|
+
"zh-TW": "容器資料磁碟區並未刪除:{volumes}。其中的資料已保留。",
|
|
16
|
+
"zh-CN": "容器数据卷并未删除:{volumes}。其中的数据已保留。",
|
|
17
|
+
"ja": "コンテナのデータボリュームは削除されていません:{volumes}。その中のデータは保持されます。",
|
|
18
|
+
"ko": "컨테이너 데이터 볼륨이 삭제되지 않았습니다: {volumes}. 볼륨의 데이터는 보존됩니다.",
|
|
19
|
+
"de": "Container-Datenvolumes wurden NICHT gelöscht: {volumes}. Die darin gespeicherten Daten bleiben erhalten.",
|
|
20
|
+
"fr": "Les volumes de données des conteneurs n’ont PAS été supprimés : {volumes}. Leurs données sont conservées.",
|
|
21
|
+
"es": "Los volúmenes de datos de los contenedores NO se eliminaron: {volumes}. Sus datos se conservan.",
|
|
22
|
+
"it": "I volumi dati dei container NON sono stati eliminati: {volumes}. I dati al loro interno vengono conservati."
|
|
23
|
+
},
|
|
2
24
|
"compose.diagnosis.registry_check": {
|
|
3
25
|
"en": "Registry check",
|
|
4
26
|
"zh-TW": "登錄站檢查",
|
package/dist/ctl/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CtlError, errorData, ctlError } from './errors.js';
|
|
|
6
6
|
import { CtlStore } from './store.js';
|
|
7
7
|
import { CtlClient, boundedMap } from './service.js';
|
|
8
8
|
import { selectHosts, selectApp, selectOperation, flagName, RESERVED_FLAGS } from './input.js';
|
|
9
|
-
import { canonicalOrigin, createRequest } from './transport.js';
|
|
9
|
+
import { canonicalOrigin, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
10
10
|
import { loginDsm, logoutDsm } from './dsm-auth.js';
|
|
11
11
|
import { publicOperation } from './redaction.js';
|
|
12
12
|
const USAGE = 'synopkgland ctl host add|list|show|login|logout|remove|revoke; ctl list [--host a,b|--all-hosts] [--app pkg] [--refresh]; ctl <alias|package> <command...> [typed flags|--input file|-] [--dry-run] [--yes]';
|
|
@@ -70,7 +70,11 @@ export async function runCtl(argv, io = {}) {
|
|
|
70
70
|
emit({ ok: true, data: { usage: USAGE } });
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
const
|
|
73
|
+
const timeout = take(args, '--timeout', true);
|
|
74
|
+
if (timeout && (!/^\d+$/.test(timeout) || Number(timeout) < 1 || Number(timeout) > 3600))
|
|
75
|
+
throw new CtlError('CTL_ARGUMENT_INVALID', '--timeout must be 1–3600 seconds.', 'Use --timeout 60.');
|
|
76
|
+
const timeoutMs = timeout ? Number(timeout) * 1000 : CTL_TIMEOUT_MS;
|
|
77
|
+
const client = (host) => new CtlClient(store, host, io.requestFor?.(host), timeoutMs);
|
|
74
78
|
const discoverOptions = { refresh, app: appFilter, approve, confirm: tty ? async (packages) => /^y(es)?$/i.test(await ask('Allow DSM session to fixed description endpoints for ' + packages.map(p => p.id + '@' + p.version).join(', ') + '? [y/N] ')) : undefined };
|
|
75
79
|
if (args[0] === 'host') {
|
|
76
80
|
const action = args[1], name = args[2];
|
|
@@ -100,7 +104,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
100
104
|
const host = old ?? { name, origin: canonicalOrigin(url, tunnel), username: usernameOption ?? await ask('DSM account: '), approved: {}, ...(tunnel ? { tunnel: true } : {}), ...(ca ? { ca } : {}) };
|
|
101
105
|
const previous = old ? await store.credentials.get(store.key(host)) : null;
|
|
102
106
|
const secret = passwordStdin ? readBounded(0, 4096).replace(/[\r\n]+$/, '') : await ask('User password: ', true);
|
|
103
|
-
const session = await loginDsm(host.origin, host.username, secret, { remember: !noRemember, deviceId: previous?.deviceId, otp: () => ask('DSM OTP: ', true), request: io.requestFor?.(host), ca: host.ca ? readFileSync(host.ca, 'utf8') : undefined });
|
|
107
|
+
const session = await loginDsm(host.origin, host.username, secret, { remember: !noRemember, deviceId: previous?.deviceId, otp: () => ask('DSM OTP: ', true), request: io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs, host.ca ? readFileSync(host.ca, 'utf8') : undefined), ca: host.ca ? readFileSync(host.ca, 'utf8') : undefined });
|
|
104
108
|
try {
|
|
105
109
|
if (old)
|
|
106
110
|
await store.credentials.set(store.key(host), session);
|
|
@@ -108,7 +112,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
108
112
|
await store.add(host, session);
|
|
109
113
|
}
|
|
110
114
|
catch (e) {
|
|
111
|
-
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin)).catch(() => { });
|
|
115
|
+
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs)).catch(() => { });
|
|
112
116
|
throw e;
|
|
113
117
|
}
|
|
114
118
|
host.lastUsed = new Date().toISOString();
|
|
@@ -135,7 +139,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
135
139
|
let logoutError;
|
|
136
140
|
if (session?.cookie) {
|
|
137
141
|
try {
|
|
138
|
-
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin,
|
|
142
|
+
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs, host.ca ? readFileSync(host.ca, 'utf8') : undefined));
|
|
139
143
|
loggedOut = true;
|
|
140
144
|
}
|
|
141
145
|
catch (error) {
|
|
@@ -184,7 +188,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
184
188
|
return { host: name, ok: true, data: path.length ? { ...view, commands: [view.commands.find(c => c.id === selectOperation(app, path).id)] } : view };
|
|
185
189
|
}
|
|
186
190
|
const tail = args.slice(1);
|
|
187
|
-
const noWait = !!take(tail, '--no-wait'), wait = !!take(tail, '--wait')
|
|
191
|
+
const noWait = !!take(tail, '--no-wait'), wait = !!take(tail, '--wait');
|
|
188
192
|
if (noWait && wait)
|
|
189
193
|
throw new CtlError('CTL_ARGUMENT_INVALID', 'Use --wait or --no-wait.', 'run: synopkgland ctl list --json');
|
|
190
194
|
if (timeout && (!/^\d+$/.test(timeout) || Number(timeout) < 1 || Number(timeout) > 3600))
|
package/dist/ctl/flows.js
CHANGED
|
@@ -32,13 +32,13 @@ export async function runFlow(flow, params, invoke, options) {
|
|
|
32
32
|
}
|
|
33
33
|
throw new CtlError('CTL_FLOW_LIMIT', 'Pagination page limit reached; result is incomplete.', 'Narrow the query.', 5);
|
|
34
34
|
}
|
|
35
|
+
const until = performance.now() + Math.min(options.timeoutMs ?? flow.timeoutMs, 3600000);
|
|
35
36
|
const accepted = await invoke(flow.start, params);
|
|
36
37
|
if (options.wait === false)
|
|
37
38
|
return { accepted: true, job: accepted };
|
|
38
39
|
const id = field(accepted, flow.jobId);
|
|
39
40
|
if (!['string', 'number'].includes(typeof id))
|
|
40
41
|
throw new CtlError('CTL_FLOW_RESPONSE', 'Start response has no job ID.', 'Check command metadata; start is not retried.', 5);
|
|
41
|
-
const until = performance.now() + Math.min(options.timeoutMs ?? flow.timeoutMs, 3600000);
|
|
42
42
|
while (performance.now() < until) {
|
|
43
43
|
const poll = await invoke(flow.poll, { [flow.idParam]: id });
|
|
44
44
|
const state = field(poll, flow.state);
|
package/dist/ctl/service.d.ts
CHANGED
|
@@ -15,7 +15,9 @@ export declare class CtlClient {
|
|
|
15
15
|
store: CtlStore;
|
|
16
16
|
host: Host;
|
|
17
17
|
request: JsonRequest;
|
|
18
|
-
constructor(store: CtlStore, host: Host, request?: JsonRequest);
|
|
18
|
+
constructor(store: CtlStore, host: Host, request?: JsonRequest, timeoutMs?: number);
|
|
19
|
+
private until?;
|
|
20
|
+
private remaining;
|
|
19
21
|
valid(): Promise<boolean>;
|
|
20
22
|
inventory(session: DsmSession): Promise<{
|
|
21
23
|
id: string;
|
|
@@ -34,5 +36,6 @@ export declare class CtlClient {
|
|
|
34
36
|
wait?: boolean;
|
|
35
37
|
timeoutMs?: number;
|
|
36
38
|
}): Promise<any>;
|
|
39
|
+
private executeWithin;
|
|
37
40
|
private appCookieSession;
|
|
38
41
|
}
|
package/dist/ctl/service.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { CtlError, errorData, choose } from './errors.js';
|
|
3
|
-
import { APP_COOKIE, createRequest } from './transport.js';
|
|
3
|
+
import { APP_COOKIE, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
4
4
|
import { DESCRIPTION_METHOD, validateDescriptor, ajv } from './descriptor.js';
|
|
5
5
|
import { runFlow } from './flows.js';
|
|
6
6
|
import { SensitiveOutput } from './redaction.js';
|
|
@@ -14,11 +14,13 @@ export class CtlClient {
|
|
|
14
14
|
store;
|
|
15
15
|
host;
|
|
16
16
|
request;
|
|
17
|
-
constructor(store, host, request) {
|
|
17
|
+
constructor(store, host, request, timeoutMs = CTL_TIMEOUT_MS) {
|
|
18
18
|
this.store = store;
|
|
19
19
|
this.host = host;
|
|
20
|
-
this.request = request ?? createRequest(host.origin,
|
|
20
|
+
this.request = request ?? createRequest(host.origin, () => this.remaining(timeoutMs), host.ca ? readFileSync(host.ca, 'utf8') : undefined);
|
|
21
21
|
}
|
|
22
|
+
until;
|
|
23
|
+
remaining(fallback) { return this.until === undefined ? fallback : Math.max(0, this.until - performance.now()); }
|
|
22
24
|
async valid() { try {
|
|
23
25
|
await this.inventory(await this.store.session(this.host));
|
|
24
26
|
return true;
|
|
@@ -102,11 +104,23 @@ export class CtlClient {
|
|
|
102
104
|
return { apps: results.flatMap(r => r.description ? [r.description] : []), diagnostics: results.flatMap(r => r.error ? [r.error] : []), cacheHits };
|
|
103
105
|
}
|
|
104
106
|
async execute(name, path, args, options = {}) {
|
|
107
|
+
try {
|
|
108
|
+
return await this.executeWithin(name, path, args, options);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
this.until = undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async executeWithin(name, path, args, options = {}) {
|
|
115
|
+
const started = performance.now();
|
|
116
|
+
this.until = started + (options.timeoutMs ?? CTL_TIMEOUT_MS);
|
|
105
117
|
const listing = await this.discover(options);
|
|
106
118
|
const app = selectApp(listing.apps, name);
|
|
107
119
|
const { id, operation } = selectOperation(app, path);
|
|
108
120
|
const params = operationInput(operation, args, options.stdin, `run: synopkgland ctl ${name} ${operation.cli.join(' ')} --host ${this.host.name} --help`);
|
|
109
121
|
const output = new SensitiveOutput(params, operation.paramsSchema);
|
|
122
|
+
if (options.timeoutMs === undefined && operation.flow.type === 'job')
|
|
123
|
+
this.until = started + operation.flow.timeoutMs;
|
|
110
124
|
try {
|
|
111
125
|
const plan = requestPlan(app, id, params);
|
|
112
126
|
if (operation.flow.type === 'upload')
|
|
@@ -135,9 +149,9 @@ export class CtlClient {
|
|
|
135
149
|
return response.data;
|
|
136
150
|
};
|
|
137
151
|
const ca = this.host.ca ? readFileSync(this.host.ca, 'utf8') : undefined;
|
|
138
|
-
const result = await runFlow(operation.flow, params, invoke, { wait: options.wait, timeoutMs: options.timeoutMs,
|
|
139
|
-
ndjson: (method, input) => transferRequest(appSession, app.payload.transport.path, method, input, { ca, timeoutMs: options.timeoutMs }),
|
|
140
|
-
upload: (method, input, flow) => transferRequest(appSession, app.payload.transport.path, method, {}, { ca, timeoutMs: options.timeoutMs, file: { path: String(input[flow.fileParam]), parent: String(input[flow.parentParam]), name: String(input[flow.nameParam]) } }) });
|
|
152
|
+
const result = await runFlow(operation.flow, params, invoke, { wait: options.wait, timeoutMs: this.remaining(options.timeoutMs ?? CTL_TIMEOUT_MS),
|
|
153
|
+
ndjson: (method, input) => transferRequest(appSession, app.payload.transport.path, method, input, { ca, timeoutMs: this.remaining(options.timeoutMs ?? CTL_TIMEOUT_MS) }),
|
|
154
|
+
upload: (method, input, flow) => transferRequest(appSession, app.payload.transport.path, method, {}, { ca, timeoutMs: this.remaining(options.timeoutMs ?? CTL_TIMEOUT_MS), file: { path: String(input[flow.fileParam]), parent: String(input[flow.parentParam]), name: String(input[flow.nameParam]) } }) });
|
|
141
155
|
if (operation.resultSchema && !ajv.validate(operation.resultSchema, result))
|
|
142
156
|
throw new CtlError('CTL_RESPONSE_INVALID', 'Result does not match command schema.', 'Check the App version.', 5);
|
|
143
157
|
return output.clean(result, operation.resultSchema);
|
package/dist/ctl/transfers.d.ts
CHANGED
package/dist/ctl/transfers.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createReadStream, statSync } from 'node:fs';
|
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import https from 'node:https';
|
|
4
4
|
import { CtlError } from './errors.js';
|
|
5
|
+
import { CTL_TIMEOUT_MS } from './transport.js';
|
|
5
6
|
export function localFile(file) { if (typeof file !== 'string' || !file)
|
|
6
7
|
throw new CtlError('CTL_INPUT_FILE', 'A local filename is required.', 'Use --file <path>.'); try {
|
|
7
8
|
const stat = statSync(file);
|
|
@@ -15,6 +16,8 @@ catch {
|
|
|
15
16
|
export async function transferRequest(session, path, method, params, options = {}) {
|
|
16
17
|
if (!/^\/webman\/3rdparty\/[A-Za-z][A-Za-z0-9_.+-]*\/cgi\/api\.cgi$/.test(path) || !/^([a-z][a-z0-9_]*\.)+[a-z][a-z0-9_]*$/.test(method))
|
|
17
18
|
throw new CtlError('CTL_PATH_INVALID', 'Invalid transfer binding.', 'Refresh the API description.', 4);
|
|
19
|
+
if (options.timeoutMs !== undefined && options.timeoutMs <= 0)
|
|
20
|
+
throw new CtlError('CTL_NETWORK', 'Transfer budget exhausted before dispatch.', 'Check App state before retrying writes.', 6);
|
|
18
21
|
const url = new URL(path, session.origin);
|
|
19
22
|
const file = options.file ? localFile(options.file.path) : undefined;
|
|
20
23
|
if (options.file) {
|
|
@@ -23,7 +26,7 @@ export async function transferRequest(session, path, method, params, options = {
|
|
|
23
26
|
const raw = file ? undefined : Buffer.from(JSON.stringify({ method, params }));
|
|
24
27
|
return new Promise((resolve, reject) => {
|
|
25
28
|
const headers = { 'content-type': file ? 'application/octet-stream' : 'application/json', cookie: session.cookie, 'X-SYNO-TOKEN': session.token, origin: session.origin, 'content-length': String(file?.size ?? raw.length) };
|
|
26
|
-
const req = (url.protocol === 'https:' ? https : http).request(url, { method: 'POST', headers, timeout:
|
|
29
|
+
const req = (url.protocol === 'https:' ? https : http).request(url, { method: 'POST', headers, timeout: options.timeoutMs ?? CTL_TIMEOUT_MS, ...(options.ca ? { ca: options.ca } : {}) }, res => {
|
|
27
30
|
const expected = file ? 'application/json' : 'application/x-ndjson';
|
|
28
31
|
if (res.statusCode !== 200 || !res.headers['content-type']?.includes(expected)) {
|
|
29
32
|
res.resume();
|
|
@@ -58,7 +61,7 @@ export async function transferRequest(session, path, method, params, options = {
|
|
|
58
61
|
reject(new CtlError('CTL_TRANSFER_FAILED', 'Invalid or failed transfer response.', 'Inspect the App result; do not retry writes blindly.', 5));
|
|
59
62
|
} });
|
|
60
63
|
});
|
|
61
|
-
const wallTimer = setTimeout(() => req.destroy(), options.timeoutMs ??
|
|
64
|
+
const wallTimer = setTimeout(() => req.destroy(), options.timeoutMs ?? CTL_TIMEOUT_MS);
|
|
62
65
|
req.once('close', () => clearTimeout(wallTimer));
|
|
63
66
|
req.on('timeout', () => req.destroy());
|
|
64
67
|
req.on('error', () => reject(new CtlError('CTL_NETWORK', 'Transfer interrupted; completion may be uncertain.', 'Check App state before retrying.', 6)));
|
package/dist/ctl/transport.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const CTL_TIMEOUT_MS = 62000;
|
|
1
2
|
export declare const APP_COOKIE: unique symbol;
|
|
2
3
|
export interface DsmSession {
|
|
3
4
|
origin: string;
|
|
@@ -8,4 +9,4 @@ export interface DsmSession {
|
|
|
8
9
|
}
|
|
9
10
|
export type JsonRequest = (path: string, body: URLSearchParams | Record<string, unknown>, session?: DsmSession) => Promise<any>;
|
|
10
11
|
export declare function canonicalOrigin(input: string, tunnel?: boolean): string;
|
|
11
|
-
export declare function createRequest(origin: string, timeoutMs?: number, ca?: string): JsonRequest;
|
|
12
|
+
export declare function createRequest(origin: string, timeoutMs?: number | (() => number), ca?: string): JsonRequest;
|
package/dist/ctl/transport.js
CHANGED
|
@@ -2,6 +2,7 @@ import { strictJson } from './json.js';
|
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import https from 'node:https';
|
|
4
4
|
import { CtlError } from './errors.js';
|
|
5
|
+
export const CTL_TIMEOUT_MS = 62000;
|
|
5
6
|
export const APP_COOKIE = Symbol("ctlAppCookie");
|
|
6
7
|
export function canonicalOrigin(input, tunnel = false) {
|
|
7
8
|
let url;
|
|
@@ -15,10 +16,13 @@ export function canonicalOrigin(input, tunnel = false) {
|
|
|
15
16
|
throw new CtlError('CTL_URL_INVALID', 'Use HTTPS, or explicit --ssh-tunnel for a loopback HTTP tunnel.', 'run: synopkgland ctl host add <name> --url https://<nas>:5001');
|
|
16
17
|
return url.origin;
|
|
17
18
|
}
|
|
18
|
-
export function createRequest(origin, timeoutMs =
|
|
19
|
+
export function createRequest(origin, timeoutMs = CTL_TIMEOUT_MS, ca) {
|
|
19
20
|
return async (path, body, session) => {
|
|
20
21
|
if (!path.startsWith('/') || path.startsWith('//') || path.includes('?') || path.includes('#') || path.includes('\\') || path.includes('%') || path.split('/').some(p => p === '.' || p === '..'))
|
|
21
22
|
throw new CtlError('CTL_PATH_INVALID', 'Unsafe request path.', 'run: synopkgland ctl list --refresh');
|
|
23
|
+
const remaining = typeof timeoutMs === 'function' ? timeoutMs() : timeoutMs;
|
|
24
|
+
if (remaining <= 0)
|
|
25
|
+
throw new CtlError('CTL_NETWORK', 'Request budget exhausted before dispatch.', 'Check App state before retrying writes.', 6);
|
|
22
26
|
const url = new URL(path, origin);
|
|
23
27
|
if (url.origin !== origin || (session && session.origin !== origin))
|
|
24
28
|
throw new CtlError('CTL_ORIGIN_MISMATCH', 'Credentials cannot cross NAS origins.', 'run: synopkgland ctl host list', 4);
|
|
@@ -32,7 +36,7 @@ export function createRequest(origin, timeoutMs = 10000, ca) {
|
|
|
32
36
|
headers.cookie = session.cookie;
|
|
33
37
|
headers['X-SYNO-TOKEN'] = session.token;
|
|
34
38
|
}
|
|
35
|
-
const req = (url.protocol === 'https:' ? https : http).request(url, { method: 'POST', headers, timeout:
|
|
39
|
+
const req = (url.protocol === 'https:' ? https : http).request(url, { method: 'POST', headers, timeout: remaining, ...(ca ? { ca } : {}) }, res => {
|
|
36
40
|
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
37
41
|
res.resume();
|
|
38
42
|
reject(new CtlError('CTL_HTTP_ERROR', `HTTP ${res.statusCode}; redirects are never followed.`, 'Check the NAS URL and connection.', 6));
|
|
@@ -69,7 +73,7 @@ export function createRequest(origin, timeoutMs = 10000, ca) {
|
|
|
69
73
|
}
|
|
70
74
|
});
|
|
71
75
|
});
|
|
72
|
-
const wallTimer = setTimeout(() => req.destroy(),
|
|
76
|
+
const wallTimer = setTimeout(() => req.destroy(), remaining);
|
|
73
77
|
req.once('close', () => clearTimeout(wallTimer));
|
|
74
78
|
req.on('timeout', () => req.destroy());
|
|
75
79
|
req.on('error', () => reject(new CtlError('CTL_NETWORK', 'NAS request failed or timed out.', 'Check the NAS connection and trusted CA; requests are not replayed.', 6)));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Only the trusted SDK planner supplies names for retained-data reporting. */
|
|
2
|
+
export interface ComposeAppData {
|
|
3
|
+
project: string;
|
|
4
|
+
volumes: Array<{
|
|
5
|
+
key: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
export declare function validComposeAppData(value: unknown, pkg: string): value is ComposeAppData;
|
|
10
|
+
export declare const COMPOSE_DELETE_CHOICE = "compose_remove_data=0\nif [ \"${SYNOPKG_PKG_STATUS:-}\" = UNINSTALL ]; then\n for flag in \"${pkgwizard_remove_additional:-}\" \"${wizard_pkgwizard_remove_additional:-}\" \"${delete_app_data:-}\" \"${wizard_delete_app_data:-}\"; do\n case \"$flag\" in true|True|TRUE|yes|1) compose_remove_data=1 ;; esac\n done\nfi\n";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function validComposeAppData(value, pkg) {
|
|
2
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3
|
+
return false;
|
|
4
|
+
const inventory = value;
|
|
5
|
+
return Object.keys(inventory).every(key => ["project", "volumes"].includes(key)) &&
|
|
6
|
+
typeof inventory.project === "string" && /^[a-z][a-z0-9-]{0,96}$/.test(inventory.project) &&
|
|
7
|
+
(inventory.project === pkg.toLowerCase() || inventory.project.endsWith("-" + pkg.toLowerCase())) &&
|
|
8
|
+
Array.isArray(inventory.volumes) && inventory.volumes.length <= 256 &&
|
|
9
|
+
new Set(inventory.volumes.map(volume => volume?.key)).size === inventory.volumes.length &&
|
|
10
|
+
inventory.volumes.every(volume => volume && Object.keys(volume).length === 2 &&
|
|
11
|
+
typeof volume.key === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(volume.key) &&
|
|
12
|
+
volume.name === inventory.project + "_" + volume.key);
|
|
13
|
+
}
|
|
14
|
+
export const COMPOSE_DELETE_CHOICE = `compose_remove_data=0
|
|
15
|
+
if [ "\${SYNOPKG_PKG_STATUS:-}" = UNINSTALL ]; then
|
|
16
|
+
for flag in "\${pkgwizard_remove_additional:-}" "\${wizard_pkgwizard_remove_additional:-}" "\${delete_app_data:-}" "\${wizard_delete_app_data:-}"; do
|
|
17
|
+
case "$flag" in true|True|TRUE|yes|1) compose_remove_data=1 ;; esac
|
|
18
|
+
done
|
|
19
|
+
fi
|
|
20
|
+
`;
|
|
@@ -42,5 +42,16 @@
|
|
|
42
42
|
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
43
43
|
},
|
|
44
44
|
"goToolchain": "go1.26.9"
|
|
45
|
+
},
|
|
46
|
+
"0.17.5": {
|
|
47
|
+
"x86_64": "af37ecd6bb5ab3e954b5f5bb3db2499b3cf86f0502afdcea18e8dc0614a46e5d",
|
|
48
|
+
"aarch64": "87436df3f591e0126d88866922b032968d0dbd01a109de53ab751ef779257baf",
|
|
49
|
+
"armv7": "de354d6e582ad1fe3124c3f30eb7f3ce8af456b0fcfad8f23c10421d3a3f3d42",
|
|
50
|
+
"lifecycle": {
|
|
51
|
+
"x86_64": "f3b368c86677741b48b059699a86c935157ea797634a6116131678e8bbaf33ed",
|
|
52
|
+
"aarch64": "b6ccd1365844e6eee9db4ce5833b655f4f1e138bf34317cdbd0e2342d3342a9e",
|
|
53
|
+
"armv7": "57b6350c2812783cb536384f8b363c26d894af6589b5f19464d71d8d0a8035d5"
|
|
54
|
+
},
|
|
55
|
+
"goToolchain": "go1.26.9"
|
|
45
56
|
}
|
|
46
57
|
}
|
|
@@ -18,6 +18,12 @@ export interface DelegateCapability {
|
|
|
18
18
|
export declare function validDelegate(value: unknown): value is DelegateCapability;
|
|
19
19
|
export declare const sha256: (data: string | Buffer) => string;
|
|
20
20
|
export interface ExecutorPolicy {
|
|
21
|
+
timeouts?: {
|
|
22
|
+
start: number;
|
|
23
|
+
stop: number;
|
|
24
|
+
purge: number;
|
|
25
|
+
operation: number;
|
|
26
|
+
};
|
|
21
27
|
webapi?: WebAPIProfiles;
|
|
22
28
|
v: 1;
|
|
23
29
|
app: string;
|
|
@@ -56,9 +62,28 @@ export declare function executorPolicy(m: {
|
|
|
56
62
|
portal?: PortalDeclaration;
|
|
57
63
|
os_min_ver?: string;
|
|
58
64
|
displayname?: string;
|
|
65
|
+
daemon?: {
|
|
66
|
+
timeouts: {
|
|
67
|
+
start: number;
|
|
68
|
+
stop: number;
|
|
69
|
+
status: number;
|
|
70
|
+
uninstall: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
lifecycleTimeouts?: {
|
|
74
|
+
start: number;
|
|
75
|
+
stop: number;
|
|
76
|
+
status: number;
|
|
77
|
+
uninstall: number;
|
|
78
|
+
};
|
|
79
|
+
backend?: {
|
|
80
|
+
kind: string;
|
|
81
|
+
timeoutSeconds?: number;
|
|
82
|
+
};
|
|
59
83
|
uninstall?: {
|
|
60
84
|
purge: {
|
|
61
85
|
root?: string;
|
|
86
|
+
timeout?: number;
|
|
62
87
|
};
|
|
63
88
|
};
|
|
64
89
|
}, arch?: NativeArchFamily): ExecutorPolicy;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationBudgetSeconds } from "./runtime-budgets.js";
|
|
1
2
|
import { validWebAPIProfiles, sortedWebAPIProfiles } from './webapi-profile.js';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { MOVE_FROM_STAGING, DELEGATE_EXTENSION_OPERATIONS } from './delegate-contract.js';
|
|
@@ -57,6 +58,12 @@ export function executorPolicy(m, arch) {
|
|
|
57
58
|
operations: [...new Set(operations)].sort(), ...(portal ? { portal } : {}), ...(purge ? { purge } : {}), ...(settings?.stop ? { stop: settings.stop } : {}),
|
|
58
59
|
...(settings?.start ? { start: settings.start } : {}),
|
|
59
60
|
...(m.capabilities.webapi ? { webapi: sortedWebAPIProfiles(m.capabilities.webapi) } : {}),
|
|
61
|
+
...((m.lifecycleTimeouts || m.daemon || m.backend?.timeoutSeconds || (m.uninstall?.purge.root && m.uninstall.purge.timeout)) ? { timeouts: {
|
|
62
|
+
start: m.lifecycleTimeouts?.start ?? m.daemon?.timeouts.start ?? 60,
|
|
63
|
+
stop: m.lifecycleTimeouts?.stop ?? m.daemon?.timeouts.stop ?? 60,
|
|
64
|
+
purge: m.lifecycleTimeouts?.uninstall ?? m.daemon?.timeouts.uninstall ?? Math.min(3600, Math.max(300, m.uninstall?.purge.timeout ?? 0)),
|
|
65
|
+
operation: operationBudgetSeconds(m),
|
|
66
|
+
} } : {}),
|
|
60
67
|
root, ...(settings?.state ? { root_state: true } : {}) };
|
|
61
68
|
}
|
|
62
69
|
/** A script without settings keeps the bare digest; a binary pins the bytes built for this architecture. */
|
|
@@ -86,7 +93,7 @@ export function validExecutorPolicy(value, pkg) {
|
|
|
86
93
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
87
94
|
return false;
|
|
88
95
|
const p = value;
|
|
89
|
-
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'stop', 'start', 'root', 'root_state', 'webapi'].includes(key))
|
|
96
|
+
if (Object.keys(p).some(key => !['timeouts', 'v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'stop', 'start', 'root', 'root_state', 'webapi'].includes(key))
|
|
90
97
|
|| p.v !== 1 || p.package !== pkg || p.app !== `SYNO.SDS.${pkg}.Instance`
|
|
91
98
|
|| 'entry' in p && p.entry !== 'admin-only' && p.entry !== 'app-privilege'
|
|
92
99
|
|| !Array.isArray(p.operations) || !p.operations.includes('status') || new Set(p.operations).size !== p.operations.length
|
|
@@ -97,6 +104,8 @@ export function validExecutorPolicy(value, pkg) {
|
|
|
97
104
|
|| 'root_state' in p && p.root_state !== true
|
|
98
105
|
|| Object.entries(p.root).some(([name, entry]) => !ROOT_PROGRAM_NAME.test(name) || rootEntryProblem(entry) !== undefined))
|
|
99
106
|
return false;
|
|
107
|
+
if (p.timeouts !== undefined && (!p.timeouts || typeof p.timeouts !== 'object' || Array.isArray(p.timeouts) || Object.keys(p.timeouts).sort().join(',') !== 'operation,purge,start,stop' || Object.entries(p.timeouts).some(([key, n]) => !Number.isInteger(n) || n < (key === 'operation' ? 1 : 2) || n > 3600)))
|
|
108
|
+
return false;
|
|
100
109
|
const rootOps = rootOperations(p.root);
|
|
101
110
|
if (![...ROOT_JOB_OPERATIONS, 'root.run'].every(op => p.operations.includes(op) === rootOps.includes(op)))
|
|
102
111
|
return false;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Work budgets use existing author settings; credential lifetimes stay separate. */
|
|
2
|
+
interface OperationBudgetSource {
|
|
3
|
+
backend?: {
|
|
4
|
+
kind: string;
|
|
5
|
+
timeoutSeconds?: number;
|
|
6
|
+
};
|
|
7
|
+
daemon?: {
|
|
8
|
+
timeouts?: {
|
|
9
|
+
status: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
lifecycleTimeouts?: {
|
|
13
|
+
status: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A CGI's declared request budget owns operation work. Binary and Compose Apps
|
|
18
|
+
* have no compiled CGI deadline to configure, so their existing status allowance
|
|
19
|
+
* can enlarge the shared operation budget above its 60-second default.
|
|
20
|
+
*/
|
|
21
|
+
export declare function operationBudgetSeconds(source: OperationBudgetSource): number;
|
|
22
|
+
/** First released SDK that decodes operation/lifecycle budget config fields. */
|
|
23
|
+
export declare function runtimeBudgetSdkSupported(sdk: string): boolean;
|
|
24
|
+
export {};
|