@synopackageland/cli 0.10.2 → 0.10.4
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 +259 -1696
- package/README.md +21 -2
- package/assets/login-style/mask.png +0 -0
- package/assets/login-style/wallpaper-thumbnail.jpg +0 -0
- package/assets/login-style/wallpaper.jpg +0 -0
- package/dist/build-spk/generator.js +10 -1
- package/dist/build-spk/login-style.d.ts +55 -0
- package/dist/build-spk/login-style.js +115 -0
- package/dist/cli.js +22 -1
- package/dist/error-registry.js +37 -1
- package/dist/host/dsm-client.js +6 -9
- package/dist/init.js +9 -5
- package/dist/runtime/delegate-contract.d.ts +2 -92
- package/dist/runtime/delegate-contract.js +2 -24
- package/dist/runtime/executor-digests.json +22 -0
- package/dist/runtime/executor-policy-check.d.ts +1 -1
- package/dist/runtime/executor-policy-check.js +5 -7
- package/dist/runtime/executor-policy.d.ts +2 -0
- package/dist/runtime/executor-policy.js +5 -1
- package/dist/runtime/root-programs.d.ts +2 -0
- package/dist/runtime/sdk-source.d.ts +4 -0
- package/dist/runtime/sdk-source.js +7 -0
- package/dist/runtime/sdk-timeouts.d.ts +23 -0
- package/dist/runtime/sdk-timeouts.js +44 -0
- package/dist/skill.js +1 -0
- package/dist/standalone/app-scripts.d.ts +26 -0
- package/dist/standalone/app-scripts.js +141 -0
- package/dist/standalone/bridge-app.d.ts +2 -0
- package/dist/standalone/bridge-app.js +9 -1
- package/dist/standalone/bridge-backend.d.ts +1 -1
- package/dist/standalone/build.js +5 -2
- package/dist/standalone/dsm-window.d.ts +14 -3
- package/dist/standalone/dsm-window.js +58 -12
- package/dist/standalone/host-adapter.d.ts +1 -1
- package/dist/standalone/host-adapter.js +1 -1
- package/dist/standalone/host-file-navigation.d.ts +6 -2
- package/dist/standalone/host-file-navigation.js +8 -2
- package/dist/standalone/inspect-spk.d.ts +9 -0
- package/dist/standalone/inspect-spk.js +60 -6
- package/dist/standalone/lifecycle.js +3 -1
- package/dist/standalone/manifest.d.ts +6 -0
- package/dist/standalone/manifest.js +51 -3
- package/dist/standalone/portal-window.d.ts +6 -6
- package/dist/standalone/portal-window.js +12 -32
- package/dist/standalone/request-timeout.d.ts +6 -3
- package/dist/standalone/request-timeout.js +10 -4
- package/dist/standalone/root-ops.d.ts +3 -2
- package/dist/standalone/root-ops.js +9 -3
- package/dist/standalone/root-window.d.ts +2 -2
- package/dist/standalone/root-window.js +191 -53
- package/dist/standalone/sdk-check.js +4 -1
- package/dist/standalone/spk.d.ts +3 -0
- package/dist/standalone/spk.js +10 -3
- package/dist/validate.js +2 -0
- package/package.json +5 -5
- package/skills/SKILL.md +3 -1
- package/skills/reference/create.md +5 -1
- package/skills/reference/identity.md +1 -1
- package/skills/reference/lifecycle.md +110 -0
- package/skills/reference/portal.md +12 -0
- package/dist/authorization-sdk.d.ts +0 -6
- package/dist/authorization-sdk.js +0 -6
- package/dist/standalone/native-authorization-window.d.ts +0 -2
- package/dist/standalone/native-authorization-window.js +0 -36
- package/dist/standalone/native-root-window.d.ts +0 -29
- package/dist/standalone/native-root-window.js +0 -307
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { HostAdapter, HostTitleResult, HostRequestResult, HostURLResult, HostEvent, HostSubscriptionResult, HostFileNavigationResult, HostUnsupportedResult } from "./host-types.js";
|
|
2
|
-
export declare const HOST_ADAPTER_RUNTIME = "function inspectHostPlatform(ctx) {\n ctx = ctx || {};\n var web = typeof window !== \"undefined\";\n var host = false;\n try {\n host = !!(web && ctx.appWindow && typeof SYNO !== \"undefined\" &&\n SYNO.SDS && typeof SYNO.SDS.AppWindow === \"function\" &&\n ctx.appWindow instanceof SYNO.SDS.AppWindow);\n } catch (error) {\n // An unsupported host constructor must not break a standalone shell.\n }\n function read(key) {\n if (!host || typeof _S !== \"function\") return null;\n try { return _S(key); } catch (error) { return null; }\n }\n function text(value) {\n return typeof value === \"string\" && /\\S/.test(value) ? value : null;\n }\n function versionPart(value) {\n if (typeof value === \"number\") {\n if (!isFinite(value) || value < 0 || Math.floor(value) !== value) return null;\n value = String(value);\n }\n return typeof value === \"string\" && value.length > 0 && !/\\D/.test(value) ? value : null;\n }\n var standalone = read(\"standalone\");\n var language = text(read(\"lang\"));\n var major = versionPart(read(\"majorversion\"));\n var minor = versionPart(read(\"minorversion\"));\n var fullVersion = text(read(\"fullversion\"));\n return Object.freeze({\n web: web,\n host: host ? \"dsm\" : web ? \"standalone-web\" : \"unavailable\",\n dsmStandalone: typeof standalone === \"boolean\" ? standalone : null,\n dsmLanguage: language,\n appLocale: text(ctx.locale),\n systemVersion: major !== null && minor !== null ? major + \".\" + minor : null,\n fullVersion: fullVersion,\n theme: null\n });\n}\n\nfunction requestHostFileNavigation(launch, path, isDirectory) {\n if (typeof path !== \"string\" || path.length > 4096 || path.charAt(0) !== \"/\" ||\n /[\\x00-\\x1f\\x7f\\\\]/.test(path)) return { status: \"invalid-input\" };\n var parts = path.split(\"/\");\n if (/^volume[0-9]+$/i.test(parts[1]) || parts.slice(1).some(function (part) {\n return !part || part === \".\" || part === \"..\";\n })) return { status: \"invalid-input\" };\n var result = launch(\"SYNO.SDS.App.FileStation3.Instance\", isDirectory ? { opendir: path } : { openfile: path });\n if (result.status !== \"requested\") return result;\n return { status: \"requested\", action: isDirectory ? \"locate-folder\" : \"highlight-file\" };\n}\n\n\nvar REVEAL_RECOVERY_TIMEOUT_MS = 5000;\nvar REVEAL_RECOVERY_POLL_MS = 100;\nvar REVEAL_RECOVERY_QUIET_MS = 300;\nvar hostFileRevealOwners = null, hostFileRevealSequence = 0;\ntry { if (typeof WeakMap === \"function\") hostFileRevealOwners = new WeakMap(); } catch (error) {}\nfunction createHostFileRevealRecovery(path, canAct) {\n var active = true, accepted = false, win = null, dom = null, tuple = null;\n var poll = null, deadline = null, removers = [], reached = false, pending = 0;\n var previous = null, quietSince = null, expires = 0, order = ++hostFileRevealSequence;\n var parent = path.slice(0, path.lastIndexOf(\"/\"));\n function cancel() {\n if (!active) return;\n active = false;\n try { if (poll !== null) clearTimeout(poll); } catch (error) {}\n try { if (deadline !== null) clearTimeout(deadline); } catch (error) {}\n poll = deadline = null;\n removers.splice(0).forEach(function (remove) { try { remove(); } catch (error) {} });\n try {\n if (win && hostFileRevealOwners.get(win).order === order) {\n // Preserve ordering for a late older callback, without retaining its closures.\n hostFileRevealOwners.set(win, { order: order, cancel: null });\n }\n } catch (error) {}\n win = dom = tuple = previous = null;\n }\n function requireMethods(target, names) {\n if (!target || names.some(function (name) { return typeof target[name] !== \"function\"; })) throw new Error(\"unsupported receiver\");\n }\n function guard(fn) {\n return function () {\n if (!active) return;\n try { fn.apply(null, arguments); } catch (error) { cancel(); }\n };\n }\n function listen(target, event, fn) {\n requireMethods(target, [\"on\", \"un\"]);\n var handler = guard(fn), remove = target.un;\n removers.push(function () { remove.call(target, event, handler, target); });\n target.on(event, handler, target);\n }\n function resetQuiet() { previous = null; quietSince = null; }\n function inspect() {\n requireMethods(win, [\"getPanelInstance\", \"getEl\", \"on\", \"un\"]);\n if (win.isDestroyed || win.destroyed || !canAct()) { cancel(); return null; }\n var element = win.getEl(), nextDom = element && element.dom;\n if (!nextDom) { if (dom) cancel(); return null; }\n requireMethods(nextDom, [\"addEventListener\", \"removeEventListener\"]);\n if (dom && nextDom !== dom) { cancel(); return null; }\n if (!dom) {\n dom = nextDom;\n [\"pointerdown\", \"mousedown\", \"touchstart\", \"keydown\", \"wheel\", \"click\", \"input\"].forEach(function (event) {\n var remove = dom.removeEventListener, handler = guard(cancel), target = dom;\n removers.push(function () { remove.call(target, event, handler, true); });\n dom.addEventListener(event, handler, true);\n });\n }\n var panel = win.getPanelInstance();\n if (!panel) { if (tuple) cancel(); return null; }\n requireMethods(panel, [\"getCurrentDir\", \"on\", \"un\"]);\n var view = panel.activeView, store = panel.activeDS;\n requireMethods(view, [\"getStore\", \"getSelectionModel\", \"scrollToItem\"]);\n requireMethods(store, [\"on\", \"un\", \"indexOfId\", \"getAt\", \"getCount\"]);\n var model = view.getSelectionModel();\n requireMethods(model, [\"on\", \"un\", \"getSelections\", \"selectRow\"]);\n if (view.getStore() !== store || panel.isDestroyed || store.isDestroyed || model.isDestroyed) { cancel(); return null; }\n if (tuple && (tuple.panel !== panel || tuple.view !== view || tuple.store !== store || tuple.model !== model)) { cancel(); return null; }\n if (!tuple) {\n tuple = { panel: panel, view: view, store: store, model: model };\n listen(panel, \"destroy\", cancel);\n listen(panel, \"onNodeClick\", function (node) {\n if (!node || !node.attributes || (reached && node.attributes.path !== parent)) cancel();\n });\n listen(store, \"beforeload\", function () {\n pending++; resetQuiet(); inspect();\n });\n listen(store, \"load\", function () {\n if (pending > 0) pending--;\n resetQuiet(); check();\n });\n [\"exception\", \"loadexception\", \"destroy\"].forEach(function (event) { listen(store, event, cancel); });\n listen(model, \"selectionchange\", function () { resetQuiet(); check(); });\n }\n var dir = panel.getCurrentDir(), params = store.baseParams;\n // New File Station panels expose their APIs before the first directory is set.\n // Keep the bound receiver guards and original deadline while startup finishes.\n if (!reached && dir === undefined && (!params || params.folder_path === undefined)) return null;\n if (!params || typeof params.folder_path !== \"string\") throw new Error(\"unsupported store path\");\n // Native reconfigure/selection events can precede the store's path assignment.\n if (dir === parent && params.folder_path === parent) reached = true;\n if (reached && (dir !== parent || params.folder_path !== parent)) { cancel(); return null; }\n var selections = model.getSelections();\n if (!Array.isArray(selections)) throw new Error(\"unsupported selection\");\n if (selections.some(function (record) { return !record || record.id !== path; })) { cancel(); return null; }\n var index = store.indexOfId(path), count = store.getCount();\n if (!Number.isInteger(index) || index < -1 || !Number.isInteger(count) || count < 0) throw new Error(\"unsupported store index\");\n var record = index >= 0 ? store.getAt(index) : null;\n if (index >= 0 && (!record || record.id !== path || index >= count)) throw new Error(\"inconsistent store\");\n return { dir: dir, folder: params.folder_path, count: count, index: index, record: record, selected: selections.length > 0 };\n }\n function check() {\n if (!active) return;\n if (Date.now() >= expires || !canAct()) { cancel(); return; }\n if (!win) return;\n var state = inspect();\n if (!active || !state) return;\n if (pending || state.dir !== parent || state.folder !== parent || state.index < 0) { resetQuiet(); return; }\n if (!previous || Object.keys(state).some(function (key) { return state[key] !== previous[key]; })) {\n previous = state; quietSince = Date.now();\n }\n if (Date.now() - quietSince < REVEAL_RECOVERY_QUIET_MS) return;\n var target = tuple, receiverWindow = win;\n cancel(); // Release every observer before the one permitted selection mutation.\n if (state.selected || !canAct()) return;\n target.model.selectRow(state.index);\n if (canAct() && !receiverWindow.isDestroyed && receiverWindow.getPanelInstance() === target.panel &&\n target.panel.activeView === target.view && target.panel.activeDS === target.store &&\n target.view.getSelectionModel() === target.model) target.view.scrollToItem(state.index);\n }\n function tick() {\n poll = null;\n try {\n check();\n if (active) poll = setTimeout(tick, REVEAL_RECOVERY_POLL_MS);\n } catch (error) { cancel(); }\n }\n function receive(instance) {\n if (!active || accepted) return;\n accepted = true;\n try {\n if (Date.now() >= expires || !canAct()) { cancel(); return; }\n win = instance && instance.window;\n requireMethods(win, [\"getPanelInstance\", \"getEl\", \"on\", \"un\"]);\n var owner = hostFileRevealOwners.get(win);\n if (owner && owner.order > order) { cancel(); return; }\n if (owner && owner.cancel) owner.cancel();\n hostFileRevealOwners.set(win, { order: order, cancel: cancel });\n [\"beforeclose\", \"close\", \"beforedestroy\", \"destroy\", \"hide\"].forEach(function (event) { listen(win, event, cancel); });\n check();\n } catch (error) { cancel(); }\n }\n try {\n if (!parent || !hostFileRevealOwners || typeof setTimeout !== \"function\" || typeof clearTimeout !== \"function\" || typeof Date.now !== \"function\") {\n cancel();\n } else {\n expires = Date.now() + REVEAL_RECOVERY_TIMEOUT_MS;\n deadline = setTimeout(cancel, REVEAL_RECOVERY_TIMEOUT_MS);\n poll = setTimeout(tick, REVEAL_RECOVERY_POLL_MS);\n }\n } catch (error) { cancel(); }\n return { receive: receive, cancel: cancel };\n}\n\nfunction createHostAdapter(ctx) {\n ctx = ctx || {};\n var disposed = false, subscriptions = [], revealRecovery = null;\n function launchApp(appId, config, receiverCallback) {\n if (disposed) return { status: \"unavailable\" };\n if (typeof appId !== \"string\" || !appId || /\\s/.test(appId)) return { status: \"invalid-input\" };\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n var sds = SYNO.SDS, map = sds.Config && sds.Config.FnMap, notifier = sds.StatusNotifier;\n if (typeof sds.AppLaunch !== \"function\" || !map ||\n !Object.prototype.hasOwnProperty.call(map, appId) || !map[appId] || !notifier ||\n typeof notifier.isAppEnabled !== \"function\" || !notifier.isAppEnabled(appId) ||\n typeof notifier.isAppLaunchable !== \"function\" || !notifier.isAppLaunchable(appId)) {\n return { status: \"unsupported\" };\n }\n if (receiverCallback) sds.AppLaunch(appId, config, undefined, receiverCallback);\n else sds.AppLaunch(appId, config);\n return { status: \"requested\" };\n } catch (error) { return { status: \"failed\" }; }\n }\n function navigateFileStation(path, isDirectory) {\n if (disposed) return { status: \"unavailable\" };\n if (revealRecovery) revealRecovery.cancel();\n revealRecovery = null;\n function launchFile(appId, config) {\n if (isDirectory) return launchApp(appId, config);\n var recovery = createHostFileRevealRecovery(path, function () { return !disposed && revealRecovery === recovery; });\n revealRecovery = recovery;\n var result = launchApp(appId, config, recovery.receive);\n if (result.status !== \"requested\") recovery.cancel();\n return result;\n }\n return requestHostFileNavigation(launchFile, path, isDirectory);\n }\n function unsupportedOperation() { return { status: disposed ? \"unavailable\" : \"unsupported\" }; }\n return Object.freeze({\n platform: function () {\n if (!disposed) return inspectHostPlatform(ctx);\n return Object.freeze({\n web: typeof window !== \"undefined\", host: \"unavailable\",\n dsmStandalone: null, dsmLanguage: null, appLocale: null,\n systemVersion: null, fullVersion: null, theme: null\n });\n },\n setTitle: function (title) {\n if (disposed) return { status: \"unavailable\" };\n if (typeof title !== \"string\") return { status: \"invalid-input\" };\n try {\n var platform = inspectHostPlatform(ctx);\n if (platform.host === \"dsm\") {\n if (typeof ctx.appWindow.setTitle !== \"function\") return { status: \"unsupported\" };\n ctx.appWindow.setTitle(title, undefined, true);\n return { status: \"ok\", target: \"dsm-window\" };\n }\n if (!platform.web) return { status: \"unavailable\" };\n if (typeof document === \"undefined\") return { status: \"unsupported\" };\n document.title = title;\n return { status: \"ok\", target: \"document\" };\n } catch (error) {\n return { status: \"failed\" };\n }\n },\n activateWindow: function () {\n if (disposed) return { status: \"unavailable\" };\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n var win = ctx.appWindow;\n var activate = typeof win.toFront === \"function\" ? win.toFront : win.activate;\n if (typeof activate !== \"function\") return { status: \"unsupported\" };\n // DSM minimizes by hiding; show preserves maximized geometry, unlike restore.\n if (win.hidden === true || win.minimized === true ||\n (typeof win.isVisible === \"function\" && win.isVisible() === false)) {\n if (typeof win.show !== \"function\") return { status: \"unsupported\" };\n win.show();\n }\n activate.call(win);\n return { status: \"requested\" };\n } catch (error) { return { status: \"failed\" }; }\n },\n requestClose: function () {\n if (disposed) return { status: \"unavailable\" };\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n var win = ctx.appWindow;\n // DSM returns a short-circuit value; only truthy means standalone main.\n if (typeof win.close !== \"function\" || typeof win.isStandaloneMainWindow !== \"function\" ||\n win.isStandaloneMainWindow()) return { status: \"unsupported\" };\n if (win.canClose === false) return { status: \"blocked\" };\n win.close();\n return { status: \"requested\" };\n } catch (error) { return { status: \"failed\" }; }\n },\n openApp: function (appId) { return launchApp(appId, {}); },\n openURL: function (raw) {\n if (disposed || typeof window === \"undefined\") return { status: \"unavailable\" };\n if (typeof raw !== \"string\" || !raw || /[\\s\\x00-\\x1f\\x7f]/.test(raw)) return { status: \"invalid-input\" };\n if (typeof URL !== \"function\") return { status: \"unsupported\" };\n var url;\n try { url = new URL(raw); } catch (error) { return { status: \"invalid-input\" }; }\n if ((url.protocol !== \"http:\" && url.protocol !== \"https:\") || url.username || url.password) return { status: \"invalid-input\" };\n if (typeof window.open !== \"function\") return { status: \"unsupported\" };\n try {\n var opened = window.open(url.href, \"_blank\", \"noopener,noreferrer\");\n return { status: \"requested\", windowHandleAvailable: !!opened };\n } catch (error) { return { status: \"failed\" }; }\n },\n openFileManager: function (path) { return navigateFileStation(path, true); },\n revealFile: function (path) { return navigateFileStation(path, false); },\n openFile: unsupportedOperation,\n showFileDetails: unsupportedOperation,\n openAppSetting: unsupportedOperation,\n subscribe: function (event, listener) {\n if (disposed) return { status: \"unavailable\" };\n if (typeof listener !== \"function\") return { status: \"invalid-input\" };\n if (event !== \"window-resize\") return { status: \"unsupported\" };\n var win, callback = listener, active = true;\n function handler() { if (!disposed && callback) callback(); }\n function unsubscribe() {\n if (!active) return;\n active = false;\n callback = null;\n var target = win;\n win = null;\n var index = subscriptions.indexOf(unsubscribe);\n if (index !== -1) subscriptions.splice(index, 1);\n try { if (target) target.un(\"resize\", handler, target); } catch (error) {}\n }\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n win = ctx.appWindow;\n if (typeof win.on !== \"function\" || typeof win.un !== \"function\") return { status: \"unsupported\" };\n subscriptions.push(unsubscribe);\n win.on(\"resize\", handler, win);\n if (disposed) return { status: \"unavailable\" };\n return Object.freeze({ status: \"subscribed\", dispose: unsubscribe });\n } catch (error) {\n unsubscribe();\n return { status: \"failed\" };\n }\n },\n dispose: function () {\n if (disposed) return;\n disposed = true;\n if (revealRecovery) revealRecovery.cancel();\n revealRecovery = null;\n subscriptions.slice().forEach(function (unsubscribe) { unsubscribe(); });\n ctx = null;\n }\n });\n}";
|
|
2
|
+
export declare const HOST_ADAPTER_RUNTIME = "function inspectHostPlatform(ctx) {\n ctx = ctx || {};\n var web = typeof window !== \"undefined\";\n var host = false;\n try {\n host = !!(web && ctx.appWindow && typeof SYNO !== \"undefined\" &&\n SYNO.SDS && typeof SYNO.SDS.AppWindow === \"function\" &&\n ctx.appWindow instanceof SYNO.SDS.AppWindow);\n } catch (error) {\n // An unsupported host constructor must not break a standalone shell.\n }\n function read(key) {\n if (!host || typeof _S !== \"function\") return null;\n try { return _S(key); } catch (error) { return null; }\n }\n function text(value) {\n return typeof value === \"string\" && /\\S/.test(value) ? value : null;\n }\n function versionPart(value) {\n if (typeof value === \"number\") {\n if (!isFinite(value) || value < 0 || Math.floor(value) !== value) return null;\n value = String(value);\n }\n return typeof value === \"string\" && value.length > 0 && !/\\D/.test(value) ? value : null;\n }\n var standalone = read(\"standalone\");\n var language = text(read(\"lang\"));\n var major = versionPart(read(\"majorversion\"));\n var minor = versionPart(read(\"minorversion\"));\n var fullVersion = text(read(\"fullversion\"));\n return Object.freeze({\n web: web,\n host: host ? \"dsm\" : web ? \"standalone-web\" : \"unavailable\",\n dsmStandalone: typeof standalone === \"boolean\" ? standalone : null,\n dsmLanguage: language,\n appLocale: text(ctx.locale),\n systemVersion: major !== null && minor !== null ? major + \".\" + minor : null,\n fullVersion: fullVersion,\n theme: null\n });\n}\n\nfunction requestHostFileNavigation(launch, path, isDirectory) {\n if (typeof path !== \"string\" || path.length > 4096 || path.charAt(0) !== \"/\" ||\n /[\\x00-\\x1f\\x7f\\\\]/.test(path)) return { status: \"invalid-input\" };\n var volume = /^\\/volume(?:USB|SATA)?[0-9]+(?=\\/|$)/.exec(path);\n if (volume) path = path.slice(volume[0].length);\n var parts = path.split(\"/\");\n if (parts.length < 2 || parts.slice(1).some(function (part) {\n return !part || part === \".\" || part === \"..\";\n })) return { status: \"invalid-input\" };\n var result = launch(\"SYNO.SDS.App.FileStation3.Instance\", isDirectory ? { opendir: path } : { openfile: path });\n if (result.status !== \"requested\") return result;\n return { status: \"requested\", action: isDirectory ? \"locate-folder\" : \"highlight-file\" };\n}\n\n\nvar REVEAL_RECOVERY_TIMEOUT_MS = 5000;\nvar REVEAL_RECOVERY_POLL_MS = 100;\nvar REVEAL_RECOVERY_QUIET_MS = 300;\nvar hostFileRevealOwners = null, hostFileRevealSequence = 0;\ntry { if (typeof WeakMap === \"function\") hostFileRevealOwners = new WeakMap(); } catch (error) {}\nfunction createHostFileRevealRecovery(path, canAct) {\n var active = true, accepted = false, win = null, dom = null, tuple = null;\n var poll = null, deadline = null, removers = [], reached = false, pending = 0;\n var previous = null, quietSince = null, expires = 0, order = ++hostFileRevealSequence;\n var parent = path.slice(0, path.lastIndexOf(\"/\"));\n function cancel() {\n if (!active) return;\n active = false;\n try { if (poll !== null) clearTimeout(poll); } catch (error) {}\n try { if (deadline !== null) clearTimeout(deadline); } catch (error) {}\n poll = deadline = null;\n removers.splice(0).forEach(function (remove) { try { remove(); } catch (error) {} });\n try {\n if (win && hostFileRevealOwners.get(win).order === order) {\n // Preserve ordering for a late older callback, without retaining its closures.\n hostFileRevealOwners.set(win, { order: order, cancel: null });\n }\n } catch (error) {}\n win = dom = tuple = previous = null;\n }\n function requireMethods(target, names) {\n if (!target || names.some(function (name) { return typeof target[name] !== \"function\"; })) throw new Error(\"unsupported receiver\");\n }\n function guard(fn) {\n return function () {\n if (!active) return;\n try { fn.apply(null, arguments); } catch (error) { cancel(); }\n };\n }\n function listen(target, event, fn) {\n requireMethods(target, [\"on\", \"un\"]);\n var handler = guard(fn), remove = target.un;\n removers.push(function () { remove.call(target, event, handler, target); });\n target.on(event, handler, target);\n }\n function resetQuiet() { previous = null; quietSince = null; }\n function inspect() {\n requireMethods(win, [\"getPanelInstance\", \"getEl\", \"on\", \"un\"]);\n if (win.isDestroyed || win.destroyed || !canAct()) { cancel(); return null; }\n var element = win.getEl(), nextDom = element && element.dom;\n if (!nextDom) { if (dom) cancel(); return null; }\n requireMethods(nextDom, [\"addEventListener\", \"removeEventListener\"]);\n if (dom && nextDom !== dom) { cancel(); return null; }\n if (!dom) {\n dom = nextDom;\n [\"pointerdown\", \"mousedown\", \"touchstart\", \"keydown\", \"wheel\", \"click\", \"input\"].forEach(function (event) {\n var remove = dom.removeEventListener, handler = guard(cancel), target = dom;\n removers.push(function () { remove.call(target, event, handler, true); });\n dom.addEventListener(event, handler, true);\n });\n }\n var panel = win.getPanelInstance();\n if (!panel) { if (tuple) cancel(); return null; }\n requireMethods(panel, [\"getCurrentDir\", \"on\", \"un\"]);\n var view = panel.activeView, store = panel.activeDS;\n requireMethods(view, [\"getStore\", \"getSelectionModel\", \"scrollToItem\"]);\n requireMethods(store, [\"on\", \"un\", \"indexOfId\", \"getAt\", \"getCount\"]);\n var model = view.getSelectionModel();\n requireMethods(model, [\"on\", \"un\", \"getSelections\", \"selectRow\"]);\n if (view.getStore() !== store || panel.isDestroyed || store.isDestroyed || model.isDestroyed) { cancel(); return null; }\n if (tuple && (tuple.panel !== panel || tuple.view !== view || tuple.store !== store || tuple.model !== model)) { cancel(); return null; }\n if (!tuple) {\n tuple = { panel: panel, view: view, store: store, model: model };\n listen(panel, \"destroy\", cancel);\n listen(panel, \"onNodeClick\", function (node) {\n if (!node || !node.attributes || (reached && node.attributes.path !== parent)) cancel();\n });\n listen(store, \"beforeload\", function () {\n pending++; resetQuiet(); inspect();\n });\n listen(store, \"load\", function () {\n if (pending > 0) pending--;\n resetQuiet(); check();\n });\n [\"exception\", \"loadexception\", \"destroy\"].forEach(function (event) { listen(store, event, cancel); });\n listen(model, \"selectionchange\", function () { resetQuiet(); check(); });\n }\n var dir = panel.getCurrentDir(), params = store.baseParams;\n // New File Station panels expose their APIs before the first directory is set.\n // Keep the bound receiver guards and original deadline while startup finishes.\n if (!reached && dir === undefined && (!params || params.folder_path === undefined)) return null;\n if (!params || typeof params.folder_path !== \"string\") throw new Error(\"unsupported store path\");\n // Native reconfigure/selection events can precede the store's path assignment.\n if (dir === parent && params.folder_path === parent) reached = true;\n if (reached && (dir !== parent || params.folder_path !== parent)) { cancel(); return null; }\n var selections = model.getSelections();\n if (!Array.isArray(selections)) throw new Error(\"unsupported selection\");\n if (selections.some(function (record) { return !record || record.id !== path; })) { cancel(); return null; }\n var index = store.indexOfId(path), count = store.getCount();\n if (!Number.isInteger(index) || index < -1 || !Number.isInteger(count) || count < 0) throw new Error(\"unsupported store index\");\n var record = index >= 0 ? store.getAt(index) : null;\n if (index >= 0 && (!record || record.id !== path || index >= count)) throw new Error(\"inconsistent store\");\n return { dir: dir, folder: params.folder_path, count: count, index: index, record: record, selected: selections.length > 0 };\n }\n function check() {\n if (!active) return;\n if (Date.now() >= expires || !canAct()) { cancel(); return; }\n if (!win) return;\n var state = inspect();\n if (!active || !state) return;\n if (pending || state.dir !== parent || state.folder !== parent || state.index < 0) { resetQuiet(); return; }\n if (!previous || Object.keys(state).some(function (key) { return state[key] !== previous[key]; })) {\n previous = state; quietSince = Date.now();\n }\n if (Date.now() - quietSince < REVEAL_RECOVERY_QUIET_MS) return;\n var target = tuple, receiverWindow = win;\n cancel(); // Release every observer before the one permitted selection mutation.\n if (state.selected || !canAct()) return;\n target.model.selectRow(state.index);\n if (canAct() && !receiverWindow.isDestroyed && receiverWindow.getPanelInstance() === target.panel &&\n target.panel.activeView === target.view && target.panel.activeDS === target.store &&\n target.view.getSelectionModel() === target.model) target.view.scrollToItem(state.index);\n }\n function tick() {\n poll = null;\n try {\n check();\n if (active) poll = setTimeout(tick, REVEAL_RECOVERY_POLL_MS);\n } catch (error) { cancel(); }\n }\n function receive(instance) {\n if (!active || accepted) return;\n accepted = true;\n try {\n if (Date.now() >= expires || !canAct()) { cancel(); return; }\n win = instance && instance.window;\n requireMethods(win, [\"getPanelInstance\", \"getEl\", \"on\", \"un\"]);\n var owner = hostFileRevealOwners.get(win);\n if (owner && owner.order > order) { cancel(); return; }\n if (owner && owner.cancel) owner.cancel();\n hostFileRevealOwners.set(win, { order: order, cancel: cancel });\n [\"beforeclose\", \"close\", \"beforedestroy\", \"destroy\", \"hide\"].forEach(function (event) { listen(win, event, cancel); });\n check();\n } catch (error) { cancel(); }\n }\n try {\n if (!parent || !hostFileRevealOwners || typeof setTimeout !== \"function\" || typeof clearTimeout !== \"function\" || typeof Date.now !== \"function\") {\n cancel();\n } else {\n expires = Date.now() + REVEAL_RECOVERY_TIMEOUT_MS;\n deadline = setTimeout(cancel, REVEAL_RECOVERY_TIMEOUT_MS);\n poll = setTimeout(tick, REVEAL_RECOVERY_POLL_MS);\n }\n } catch (error) { cancel(); }\n return { receive: receive, cancel: cancel };\n}\n\nfunction createHostAdapter(ctx) {\n ctx = ctx || {};\n var disposed = false, subscriptions = [], revealRecovery = null;\n function launchApp(appId, config, receiverCallback) {\n if (disposed) return { status: \"unavailable\" };\n if (typeof appId !== \"string\" || !appId || /\\s/.test(appId)) return { status: \"invalid-input\" };\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n var sds = SYNO.SDS, map = sds.Config && sds.Config.FnMap, notifier = sds.StatusNotifier;\n if (typeof sds.AppLaunch !== \"function\" || !map ||\n !Object.prototype.hasOwnProperty.call(map, appId) || !map[appId] || !notifier ||\n typeof notifier.isAppEnabled !== \"function\" || !notifier.isAppEnabled(appId) ||\n typeof notifier.isAppLaunchable !== \"function\" || !notifier.isAppLaunchable(appId)) {\n return { status: \"unsupported\" };\n }\n if (receiverCallback) sds.AppLaunch(appId, config, undefined, receiverCallback);\n else sds.AppLaunch(appId, config);\n return { status: \"requested\" };\n } catch (error) { return { status: \"failed\" }; }\n }\n function navigateFileStation(path, isDirectory) {\n if (disposed) return { status: \"unavailable\" };\n if (revealRecovery) revealRecovery.cancel();\n revealRecovery = null;\n function launchFile(appId, config) {\n if (isDirectory) return launchApp(appId, config);\n var recovery = createHostFileRevealRecovery(config.openfile, function () { return !disposed && revealRecovery === recovery; });\n revealRecovery = recovery;\n var result = launchApp(appId, config, recovery.receive);\n if (result.status !== \"requested\") recovery.cancel();\n return result;\n }\n return requestHostFileNavigation(launchFile, path, isDirectory);\n }\n function unsupportedOperation() { return { status: disposed ? \"unavailable\" : \"unsupported\" }; }\n return Object.freeze({\n platform: function () {\n if (!disposed) return inspectHostPlatform(ctx);\n return Object.freeze({\n web: typeof window !== \"undefined\", host: \"unavailable\",\n dsmStandalone: null, dsmLanguage: null, appLocale: null,\n systemVersion: null, fullVersion: null, theme: null\n });\n },\n setTitle: function (title) {\n if (disposed) return { status: \"unavailable\" };\n if (typeof title !== \"string\") return { status: \"invalid-input\" };\n try {\n var platform = inspectHostPlatform(ctx);\n if (platform.host === \"dsm\") {\n if (typeof ctx.appWindow.setTitle !== \"function\") return { status: \"unsupported\" };\n ctx.appWindow.setTitle(title, undefined, true);\n return { status: \"ok\", target: \"dsm-window\" };\n }\n if (!platform.web) return { status: \"unavailable\" };\n if (typeof document === \"undefined\") return { status: \"unsupported\" };\n document.title = title;\n return { status: \"ok\", target: \"document\" };\n } catch (error) {\n return { status: \"failed\" };\n }\n },\n activateWindow: function () {\n if (disposed) return { status: \"unavailable\" };\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n var win = ctx.appWindow;\n var activate = typeof win.toFront === \"function\" ? win.toFront : win.activate;\n if (typeof activate !== \"function\") return { status: \"unsupported\" };\n // DSM minimizes by hiding; show preserves maximized geometry, unlike restore.\n if (win.hidden === true || win.minimized === true ||\n (typeof win.isVisible === \"function\" && win.isVisible() === false)) {\n if (typeof win.show !== \"function\") return { status: \"unsupported\" };\n win.show();\n }\n activate.call(win);\n return { status: \"requested\" };\n } catch (error) { return { status: \"failed\" }; }\n },\n requestClose: function () {\n if (disposed) return { status: \"unavailable\" };\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n var win = ctx.appWindow;\n // DSM returns a short-circuit value; only truthy means standalone main.\n if (typeof win.close !== \"function\" || typeof win.isStandaloneMainWindow !== \"function\" ||\n win.isStandaloneMainWindow()) return { status: \"unsupported\" };\n if (win.canClose === false) return { status: \"blocked\" };\n win.close();\n return { status: \"requested\" };\n } catch (error) { return { status: \"failed\" }; }\n },\n openApp: function (appId) { return launchApp(appId, {}); },\n openURL: function (raw) {\n if (disposed || typeof window === \"undefined\") return { status: \"unavailable\" };\n if (typeof raw !== \"string\" || !raw || /[\\s\\x00-\\x1f\\x7f]/.test(raw)) return { status: \"invalid-input\" };\n if (typeof URL !== \"function\") return { status: \"unsupported\" };\n var url;\n try { url = new URL(raw); } catch (error) { return { status: \"invalid-input\" }; }\n if ((url.protocol !== \"http:\" && url.protocol !== \"https:\") || url.username || url.password) return { status: \"invalid-input\" };\n if (typeof window.open !== \"function\") return { status: \"unsupported\" };\n try {\n var opened = window.open(url.href, \"_blank\", \"noopener,noreferrer\");\n return { status: \"requested\", windowHandleAvailable: !!opened };\n } catch (error) { return { status: \"failed\" }; }\n },\n openFileManager: function (path) { return navigateFileStation(path, true); },\n revealFile: function (path) { return navigateFileStation(path, false); },\n openFile: unsupportedOperation,\n showFileDetails: unsupportedOperation,\n openAppSetting: unsupportedOperation,\n subscribe: function (event, listener) {\n if (disposed) return { status: \"unavailable\" };\n if (typeof listener !== \"function\") return { status: \"invalid-input\" };\n if (event !== \"window-resize\") return { status: \"unsupported\" };\n var win, callback = listener, active = true;\n function handler() { if (!disposed && callback) callback(); }\n function unsubscribe() {\n if (!active) return;\n active = false;\n callback = null;\n var target = win;\n win = null;\n var index = subscriptions.indexOf(unsubscribe);\n if (index !== -1) subscriptions.splice(index, 1);\n try { if (target) target.un(\"resize\", handler, target); } catch (error) {}\n }\n try {\n if (inspectHostPlatform(ctx).host !== \"dsm\") return { status: \"unsupported\" };\n win = ctx.appWindow;\n if (typeof win.on !== \"function\" || typeof win.un !== \"function\") return { status: \"unsupported\" };\n subscriptions.push(unsubscribe);\n win.on(\"resize\", handler, win);\n if (disposed) return { status: \"unavailable\" };\n return Object.freeze({ status: \"subscribed\", dispose: unsubscribe });\n } catch (error) {\n unsubscribe();\n return { status: \"failed\" };\n }\n },\n dispose: function () {\n if (disposed) return;\n disposed = true;\n if (revealRecovery) revealRecovery.cancel();\n revealRecovery = null;\n subscriptions.slice().forEach(function (unsubscribe) { unsubscribe(); });\n ctx = null;\n }\n });\n}";
|
|
3
3
|
/** A static, per-package factory for independent web shells; no Ext or transport. */
|
|
4
4
|
export declare function renderHostAdapterScript(packageId: string): string;
|
|
5
5
|
export declare function hostScriptName(build: number): string;
|
|
@@ -29,7 +29,7 @@ function createHostAdapter(ctx) {
|
|
|
29
29
|
revealRecovery = null;
|
|
30
30
|
function launchFile(appId, config) {
|
|
31
31
|
if (isDirectory) return launchApp(appId, config);
|
|
32
|
-
var recovery = createHostFileRevealRecovery(
|
|
32
|
+
var recovery = createHostFileRevealRecovery(config.openfile, function () { return !disposed && revealRecovery === recovery; });
|
|
33
33
|
revealRecovery = recovery;
|
|
34
34
|
var result = launchApp(appId, config, recovery.receive);
|
|
35
35
|
if (result.status !== "requested") recovery.cancel();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared virtual-path validation and File Station request shaping. The rule is
|
|
3
|
+
* the SDK's (ts/src/dsm/file-station.ts): a DSM volume mount is stripped, and
|
|
4
|
+
* anything but one canonical shared-folder path is invalid-input.
|
|
5
|
+
*/
|
|
6
|
+
export declare const HOST_FILE_NAVIGATION_RUNTIME = "\nfunction requestHostFileNavigation(launch, path, isDirectory) {\n if (typeof path !== \"string\" || path.length > 4096 || path.charAt(0) !== \"/\" ||\n /[\\x00-\\x1f\\x7f\\\\]/.test(path)) return { status: \"invalid-input\" };\n var volume = /^\\/volume(?:USB|SATA)?[0-9]+(?=\\/|$)/.exec(path);\n if (volume) path = path.slice(volume[0].length);\n var parts = path.split(\"/\");\n if (parts.length < 2 || parts.slice(1).some(function (part) {\n return !part || part === \".\" || part === \"..\";\n })) return { status: \"invalid-input\" };\n var result = launch(\"SYNO.SDS.App.FileStation3.Instance\", isDirectory ? { opendir: path } : { openfile: path });\n if (result.status !== \"requested\") return result;\n return { status: \"requested\", action: isDirectory ? \"locate-folder\" : \"highlight-file\" };\n}\n";
|
|
3
7
|
export declare const REVEAL_RECOVERY_TIMEOUT_MS = 5000;
|
|
4
8
|
export declare const REVEAL_RECOVERY_POLL_MS = 100;
|
|
5
9
|
export declare const REVEAL_RECOVERY_QUIET_MS = 300;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Shared virtual-path validation and File Station request shaping. The rule is
|
|
3
|
+
* the SDK's (ts/src/dsm/file-station.ts): a DSM volume mount is stripped, and
|
|
4
|
+
* anything but one canonical shared-folder path is invalid-input.
|
|
5
|
+
*/
|
|
2
6
|
export const HOST_FILE_NAVIGATION_RUNTIME = `
|
|
3
7
|
function requestHostFileNavigation(launch, path, isDirectory) {
|
|
4
8
|
if (typeof path !== "string" || path.length > 4096 || path.charAt(0) !== "/" ||
|
|
5
9
|
/[\\x00-\\x1f\\x7f\\\\]/.test(path)) return { status: "invalid-input" };
|
|
10
|
+
var volume = /^\\/volume(?:USB|SATA)?[0-9]+(?=\\/|$)/.exec(path);
|
|
11
|
+
if (volume) path = path.slice(volume[0].length);
|
|
6
12
|
var parts = path.split("/");
|
|
7
|
-
if (
|
|
13
|
+
if (parts.length < 2 || parts.slice(1).some(function (part) {
|
|
8
14
|
return !part || part === "." || part === "..";
|
|
9
15
|
})) return { status: "invalid-input" };
|
|
10
16
|
var result = launch("SYNO.SDS.App.FileStation3.Instance", isDirectory ? { opendir: path } : { openfile: path });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExecutorInspection } from "../runtime/executor-inspect.js";
|
|
2
|
+
import { type AppScriptName } from "./app-scripts.js";
|
|
2
3
|
import type { EntryMode } from "./manifest.js";
|
|
3
4
|
import { type UninstallCleanupItem } from "../runtime/uninstall-cleanup.js";
|
|
4
5
|
export type SpkKind = "native-go" | "native-python" | "native-node" | "native-rust" | "binary" | "container" | "native-layout";
|
|
@@ -30,11 +31,19 @@ export interface SpkInspection {
|
|
|
30
31
|
};
|
|
31
32
|
/** Declared `entry` mode; `admin-only` is the administrator floor of every route. */
|
|
32
33
|
entry?: EntryMode;
|
|
34
|
+
/** The App's own scripts/app/<DSM script name>, each called from the generated script of that name. */
|
|
35
|
+
appScripts?: AppScriptName[];
|
|
33
36
|
/** PACKAGE_ICON_256.PNG: the SDK default, a custom icon (by digest) or none. */
|
|
34
37
|
icon?: {
|
|
35
38
|
source: "default" | "custom" | "none";
|
|
36
39
|
sha256?: string;
|
|
37
40
|
};
|
|
41
|
+
/** The launcher's Login Portal style: its colour, whether the wallpaper is the CLI default, and what titles the login page. */
|
|
42
|
+
loginStyle?: {
|
|
43
|
+
color: string;
|
|
44
|
+
wallpaper: "default" | "custom";
|
|
45
|
+
title: "custom" | "app-icon" | "none";
|
|
46
|
+
};
|
|
38
47
|
}
|
|
39
48
|
/** Reads an SPK without installing it and reports how it would run on DSM. */
|
|
40
49
|
export declare function inspectStandaloneSpk(spkPath: string): Promise<SpkInspection>;
|
|
@@ -2,7 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { inspectNativeLayout } from "./native-layout.js";
|
|
3
3
|
import { checkBridgeApp, checkBridgeExecutor } from "./inspect-bridge.js";
|
|
4
4
|
import { inspectExecutor, inspectExecutorAnchor } from "../runtime/executor-inspect.js";
|
|
5
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
6
|
+
import { unwrapAppScripts } from "./app-scripts.js";
|
|
6
7
|
import { tmpdir } from "node:os";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import * as tar from "tar";
|
|
@@ -26,6 +27,7 @@ import { uninstallCleanup } from "../runtime/uninstall-cleanup.js";
|
|
|
26
27
|
import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep } from "../portal/package.js";
|
|
27
28
|
import { isPortalAlias } from "../portal/alias.js";
|
|
28
29
|
import { defaultPackageIcons, MAX_ICON_BYTES } from "../build-spk/dsm-icons.js";
|
|
30
|
+
import { isDefaultLoginAsset } from "../build-spk/login-style.js";
|
|
29
31
|
import { decodePng } from "../build-spk/png.js";
|
|
30
32
|
/** The SPK kind of each sdk-package.json backend a CGI is built for: Go itself, or the runtime of an App in another language. */
|
|
31
33
|
const COMPILED_KIND = { go: "native-go", python: "native-python", node: "native-node", rust: "native-rust" };
|
|
@@ -80,6 +82,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
80
82
|
const info = readInfo(join(dir, "INFO"), findings, kind);
|
|
81
83
|
await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
|
|
82
84
|
const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
|
|
85
|
+
// Before any fixed-lifecycle comparison: those see the SDK steps alone.
|
|
86
|
+
const appScripts = standalone || kind === "container" ? unwrapAppScripts(join(dir, "scripts"), findings) : [];
|
|
83
87
|
const sharedFolder = standalone ? recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))) : undefined;
|
|
84
88
|
// A Compose App has no executor, so its SPK never carries a portal.
|
|
85
89
|
const portal = standalone ? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings) : undefined;
|
|
@@ -136,6 +140,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
136
140
|
}
|
|
137
141
|
if (metadata.entry)
|
|
138
142
|
await checkEntryLauncher(dir, info.package, metadata.entry, findings);
|
|
143
|
+
const loginStyle = standalone ? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings) : undefined;
|
|
139
144
|
if (standalone)
|
|
140
145
|
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === 'install-selected', portal, hasExecutor);
|
|
141
146
|
const icon = inspectPackageIcons(dir, findings);
|
|
@@ -153,6 +158,8 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
153
158
|
...(portal ? { portal: { appId: portal.appId, alias: portal.alias } } : {}),
|
|
154
159
|
...(executor?.policy.portal ? { portal: { appId: executor.policy.app, alias: executor.policy.portal.alias } } : {}),
|
|
155
160
|
...(metadata.entry ? { entry: metadata.entry } : {}),
|
|
161
|
+
...(appScripts.length ? { appScripts } : {}),
|
|
162
|
+
...(loginStyle ? { loginStyle } : {}),
|
|
156
163
|
...(standalone ? { uninstallCleanup: uninstallCleanup(info.package, { executor: hasExecutor, portal: Boolean(portal), portalEntry: Boolean(executor?.policy.portal) }) } : {}),
|
|
157
164
|
package: info.package,
|
|
158
165
|
version: info.version,
|
|
@@ -185,6 +192,47 @@ async function checkEntryLauncher(dir, pkg, entry, findings) {
|
|
|
185
192
|
findings.push(`ENTRY_LAUNCHER_MISMATCH sdk-package.json declares entry: ${entry}, but the ui/config launcher is not ${JSON.stringify(expected)}; regenerate it with synopkgland build`);
|
|
186
193
|
}
|
|
187
194
|
}
|
|
195
|
+
const LOGIN_STYLE_FILES = ["defaultLoginWallpaper", "defaultLoginWallpaperThumbnail", "loginPreviewThumbnailMaskPath", "titleImgPath"];
|
|
196
|
+
/**
|
|
197
|
+
* The launcher's loginStyle and the ui/ files it names, at 1x and 2x. A portal
|
|
198
|
+
* App without one shows a blank Control Panel > Login Portal thumbnail.
|
|
199
|
+
*/
|
|
200
|
+
async function inspectLoginStyle(dir, pkg, portal, findings) {
|
|
201
|
+
const target = join(dir, "login-style-inspection");
|
|
202
|
+
mkdirSync(target);
|
|
203
|
+
await tar.x({ file: join(dir, "package.tgz"), cwd: target, filter: path => path.replace(/^\.\//, "").startsWith("ui/") });
|
|
204
|
+
let launcher;
|
|
205
|
+
try {
|
|
206
|
+
const config = JSON.parse(readFileSync(join(target, "ui/config"), "utf8"));
|
|
207
|
+
launcher = Object.values(config)[0]?.[`SYNO.SDS.${pkg}.Instance`];
|
|
208
|
+
}
|
|
209
|
+
catch { /* reported by the other checks */ }
|
|
210
|
+
const style = launcher?.loginStyle;
|
|
211
|
+
if (typeof style !== "object" || style === null) {
|
|
212
|
+
if (portal)
|
|
213
|
+
findings.push("LOGIN_STYLE_MISSING the Application Portal launcher has no loginStyle in ui/config, so Control Panel > Login Portal shows a blank thumbnail; rebuild with synopkgland build");
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const fields = style;
|
|
217
|
+
const read = (field) => {
|
|
218
|
+
const path = fields[field];
|
|
219
|
+
if (path === undefined)
|
|
220
|
+
return undefined;
|
|
221
|
+
const files = typeof path === "string" && !path.split("/").includes("..") && !path.startsWith("/")
|
|
222
|
+
? ["1x", "2x"].map(scale => join(target, "ui", path.replace("{0}", scale))) : [];
|
|
223
|
+
if (!files.length || !files.every(file => existsSync(file))) {
|
|
224
|
+
findings.push(`LOGIN_STYLE_FILE_MISSING ui/config loginStyle.${field} names ui/${String(path)}, which the payload does not carry`);
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return readFileSync(files[0]);
|
|
228
|
+
};
|
|
229
|
+
const [wallpaper] = LOGIN_STYLE_FILES.map(read);
|
|
230
|
+
return {
|
|
231
|
+
color: String(fields.mainColor ?? ""),
|
|
232
|
+
wallpaper: wallpaper && isDefaultLoginAsset("wallpaper.jpg", wallpaper) ? "default" : "custom",
|
|
233
|
+
title: fields.titleImgPath !== undefined ? "custom" : launcher.loginIcon !== undefined ? "app-icon" : "none",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
188
236
|
/** Package Center shows PACKAGE_ICON at 64 and 256 px; the package source refuses a release whose icon is over 256 KiB. */
|
|
189
237
|
function inspectPackageIcons(dir, findings) {
|
|
190
238
|
for (const [name, size] of [["PACKAGE_ICON.PNG", 64], ["PACKAGE_ICON_256.PNG", 256]]) {
|
|
@@ -479,12 +527,18 @@ function checkScripts(dir, findings) {
|
|
|
479
527
|
if (!existsSync(join(scriptsDir, "start-stop-status"))) {
|
|
480
528
|
findings.push("scripts/start-stop-status is missing");
|
|
481
529
|
}
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
485
|
-
|
|
530
|
+
const scan = (relative) => {
|
|
531
|
+
const path = join(dir, relative);
|
|
532
|
+
if (lstatSync(path).isDirectory()) {
|
|
533
|
+
for (const name of readdirSync(path))
|
|
534
|
+
scan(`${relative}/${name}`);
|
|
486
535
|
}
|
|
487
|
-
|
|
536
|
+
else if (ROOT_SCRIPT_HINTS.some((pattern) => pattern.test(readFileSync(path, "utf8")))) {
|
|
537
|
+
findings.push(`${relative} tries to gain root (sudo, chown root or setuid)`);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
if (existsSync(scriptsDir))
|
|
541
|
+
scan("scripts");
|
|
488
542
|
}
|
|
489
543
|
const DATA_KEYS = ["pkgwizard_remove_additional", "delete_app_data"];
|
|
490
544
|
const DELETE_COMMAND = /\brm\s+-[a-zA-Z]*[rR]|\bfind\b[^\n]*(-delete|-exec\s+rm)/;
|
|
@@ -5,12 +5,14 @@ import { renderDelegateInstallDraft } from '../build-spk/delegate-scope.js';
|
|
|
5
5
|
import { renderPortalLifecycle } from "../portal/package.js";
|
|
6
6
|
import { nativePortal } from "./portal.js";
|
|
7
7
|
import { renderUninstallHook } from "./uninstall-hook.js";
|
|
8
|
+
import { composeAppScript } from "./app-scripts.js";
|
|
8
9
|
const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_uninstall_hook\n';
|
|
9
10
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
10
11
|
export function renderStandaloneScript(name, manifest) {
|
|
11
12
|
const portal = nativePortal(manifest);
|
|
12
13
|
const script = renderCapabilityScript(name, manifest);
|
|
13
|
-
|
|
14
|
+
const sdk = portal ? script.replace("#!/bin/sh\n", () => `#!/bin/sh\n${renderPortalLifecycle(name, portal)}`) : script;
|
|
15
|
+
return manifest.appScripts?.some(app => app === name) ? composeAppScript(name, sdk) : sdk;
|
|
14
16
|
}
|
|
15
17
|
function renderCapabilityScript(name, manifest) {
|
|
16
18
|
if (name === 'preupgrade' && (manifest.capabilities.rootOps || manifest.capabilities.delegate)) {
|
|
@@ -8,10 +8,12 @@ import { type RootOp } from "./root-ops.js";
|
|
|
8
8
|
import type { RootSettings } from "../runtime/root-programs.js";
|
|
9
9
|
import { type Daemon } from "./daemon.js";
|
|
10
10
|
import { type Uninstall } from "./uninstall.js";
|
|
11
|
+
import { type AppScriptName } from "./app-scripts.js";
|
|
11
12
|
import { type Background } from "./background.js";
|
|
12
13
|
import { type SharedFolder } from "./shared-folder.js";
|
|
13
14
|
import { readApiPermissions, type ApiPermissions } from "./api-permissions.js";
|
|
14
15
|
import { type PortalDeclaration } from "../portal/alias.js";
|
|
16
|
+
import { type LoginStyle } from "../build-spk/login-style.js";
|
|
15
17
|
import { type SdkBackend } from "../runtime/sdk-source.js";
|
|
16
18
|
import { type WindowSize } from "./window-size.js";
|
|
17
19
|
import { type BridgeBackend } from "./bridge-backend.js";
|
|
@@ -81,8 +83,12 @@ export interface StandaloneManifest {
|
|
|
81
83
|
entry?: EntryMode;
|
|
82
84
|
/** `portal`: registers the DSM Application Portal entry; an administrator activates it. */
|
|
83
85
|
portal?: PortalDeclaration;
|
|
86
|
+
/** The project's dsm-scripts/<DSM script name> files, each run inside the generated script of that name. */
|
|
87
|
+
appScripts?: AppScriptName[];
|
|
84
88
|
/** `icon`: a square PNG of at least 256 px, or a directory of icon_<size>.png; absent uses the SDK default. */
|
|
85
89
|
icon?: string;
|
|
90
|
+
/** `login_style`: the portal login page's colour, wallpaper and title over the CLI defaults. */
|
|
91
|
+
loginStyle?: LoginStyle;
|
|
86
92
|
}
|
|
87
93
|
/**
|
|
88
94
|
* `admin-only`: DSM administrators only. `app-privilege`: administrators and
|
|
@@ -8,17 +8,20 @@ import { checkGlossary, loadCatalogs, readInfoTranslations } from "./i18n.js";
|
|
|
8
8
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR } from "./root-ops.js";
|
|
9
9
|
import { readDaemon } from "./daemon.js";
|
|
10
10
|
import { readUninstall } from "./uninstall.js";
|
|
11
|
+
import { readAppScripts } from "./app-scripts.js";
|
|
11
12
|
import { readBackground } from "./background.js";
|
|
12
13
|
import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
|
|
13
14
|
import { readSharedFolder } from "./shared-folder.js";
|
|
14
15
|
import { API_PERMISSIONS_FILE, floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions } from "./api-permissions.js";
|
|
15
16
|
import { PORTAL_REQUIRES_EXECUTOR, portalEntryProblem, readPortalDeclaration } from "../portal/alias.js";
|
|
16
17
|
import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
18
|
+
import { LOGIN_COLOR, loginImageProblem } from "../build-spk/login-style.js";
|
|
17
19
|
import { validateReproducible } from "../reproducible-config.js";
|
|
18
20
|
import { appSdkVersion, SDK_MODULE } from "../runtime/sdk-source.js";
|
|
19
21
|
import { sdkProblems } from "./sdk-check.js";
|
|
20
22
|
import { readWindowSize } from "./window-size.js";
|
|
21
|
-
import {
|
|
23
|
+
import { appSdkTimeouts } from "../runtime/sdk-timeouts.js";
|
|
24
|
+
import { requestTimeoutSeconds, rootRunDeadlineProblem, TIMEOUT_USAGE, validTimeoutSeconds } from "./request-timeout.js";
|
|
22
25
|
import { BRIDGE_RUNTIMES, isBridgeBackend, overlaps, readBridgeBackend } from "./bridge-backend.js";
|
|
23
26
|
/**
|
|
24
27
|
* `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
|
|
@@ -27,7 +30,7 @@ import { BRIDGE_RUNTIMES, isBridgeBackend, overlaps, readBridgeBackend } from ".
|
|
|
27
30
|
export const STANDALONE_MANIFEST = "dsm-package.yaml";
|
|
28
31
|
export const KNOWN_FIELDS = new Set([
|
|
29
32
|
"schema", "package", "version", "build", "displayname", "description",
|
|
30
|
-
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "uninstall", "entry", "portal", "icon", "reproducible", "root",
|
|
33
|
+
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "uninstall", "entry", "portal", "icon", "reproducible", "root", "login_style",
|
|
31
34
|
]);
|
|
32
35
|
const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
|
|
33
36
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
@@ -91,7 +94,7 @@ export function loadStandaloneManifest(root) {
|
|
|
91
94
|
function readManifest(raw, c) {
|
|
92
95
|
for (const key of Object.keys(raw)) {
|
|
93
96
|
if (!KNOWN_FIELDS.has(key)) {
|
|
94
|
-
c.fail("STANDALONE_FIELD_UNKNOWN", `Unsupported field "${key}". A Standalone Package does not accept
|
|
97
|
+
c.fail("STANDALONE_FIELD_UNKNOWN", `Unsupported field "${key}". A Standalone Package does not accept DSM conf; put lifecycle scripts in dsm-scripts/<DSM script name>.`);
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
if (raw.reproducible !== undefined) {
|
|
@@ -139,6 +142,16 @@ function readManifest(raw, c) {
|
|
|
139
142
|
if (binaryProblem)
|
|
140
143
|
c.fail("STANDALONE_ROOT_PROGRAMS_INVALID", binaryProblem);
|
|
141
144
|
const backend = arches && readBackend(raw.backend, arches, c);
|
|
145
|
+
if (backend && backend.kind !== "binary" && rootOps) {
|
|
146
|
+
// root.run may run a program without settings (its defaults) or with run.
|
|
147
|
+
// The SDK's spec/protocol/v1 Timeouts say how long the executor and its client wait.
|
|
148
|
+
const timeouts = appSdkTimeouts(backend);
|
|
149
|
+
const runnable = rootOps.map(op => ({ op, setting: rootPrograms?.programs?.[op.name] })).filter(({ setting }) => !setting || setting.run)
|
|
150
|
+
.map(({ op, setting }) => ({ name: op.name, timeoutSeconds: setting?.run?.timeout_s ?? timeouts.rootRunDefaultSeconds }));
|
|
151
|
+
const problem = rootRunDeadlineProblem(runnable, requestTimeoutSeconds(backend), timeouts.rootRunRequestMarginSeconds);
|
|
152
|
+
if (problem)
|
|
153
|
+
c.fail("STANDALONE_REQUEST_TIMEOUT_SHORT", problem);
|
|
154
|
+
}
|
|
142
155
|
let permissions;
|
|
143
156
|
if (backend && backend.kind !== "binary") {
|
|
144
157
|
try {
|
|
@@ -200,6 +213,7 @@ function readManifest(raw, c) {
|
|
|
200
213
|
const i18n = raw.i18n === undefined ? undefined : readI18n(raw.i18n, c);
|
|
201
214
|
const icon = raw.icon === undefined ? undefined : readIcon(raw.icon, c);
|
|
202
215
|
const uninstall = readUninstall(raw.uninstall, { rootOps: capabilities.rootOps === true, ops: rootOps, en: i18n?.catalogs?.en }, e => c.fail(e.code, e.message));
|
|
216
|
+
const appScripts = readAppScripts(c.root, e => c.fail(e.code, e.message));
|
|
203
217
|
const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
|
|
204
218
|
?? c.fail("STANDALONE_PORTAL_INVALID", "portal must be {} or { alias: <1-64 lowercase alphanumerics or -, not a DSM reserved path> }.");
|
|
205
219
|
if (portal && !capabilities.rootOps && !capabilities.delegate) {
|
|
@@ -212,6 +226,7 @@ function readManifest(raw, c) {
|
|
|
212
226
|
? portalEntryProblem(packageId, { application: displayname, alias: portal.alias }) : undefined;
|
|
213
227
|
if (entryProblem)
|
|
214
228
|
c.fail("STANDALONE_PORTAL_ENTRY_INVALID", entryProblem);
|
|
229
|
+
const loginStyle = raw.login_style === undefined ? undefined : readLoginStyle(raw.login_style, raw.portal !== undefined, c);
|
|
215
230
|
if (!packageId || !version || !build || !osMinVer || !displayname || !description || !maintainer
|
|
216
231
|
|| !arches || !backend || !ui || (capabilities.rootOps === true && !rootOps)) {
|
|
217
232
|
return undefined;
|
|
@@ -241,6 +256,8 @@ function readManifest(raw, c) {
|
|
|
241
256
|
...(entry ? { entry } : {}),
|
|
242
257
|
...(portal ? { portal } : {}),
|
|
243
258
|
...(icon ? { icon } : {}),
|
|
259
|
+
...(loginStyle ? { loginStyle } : {}),
|
|
260
|
+
...(appScripts ? { appScripts } : {}),
|
|
244
261
|
};
|
|
245
262
|
}
|
|
246
263
|
function checkBridgeMethods(backend, permissions, c) {
|
|
@@ -281,6 +298,37 @@ function readIcon(value, c) {
|
|
|
281
298
|
return c.fail("STANDALONE_ICON_INVALID", `icon: ${error.message}`);
|
|
282
299
|
}
|
|
283
300
|
}
|
|
301
|
+
const LOGIN_STYLE_FIELDS = ["color", "wallpaper", "wallpaper_thumbnail", "title"];
|
|
302
|
+
/** `login_style`: a portal's colour, wallpaper (with its thumbnail) and title image, each optional. */
|
|
303
|
+
function readLoginStyle(value, portal, c) {
|
|
304
|
+
const invalid = (message) => c.fail("STANDALONE_LOGIN_STYLE_INVALID", message);
|
|
305
|
+
if (!isRecord(value))
|
|
306
|
+
return invalid("login_style must be a mapping with color, wallpaper, wallpaper_thumbnail or title.");
|
|
307
|
+
if (!portal)
|
|
308
|
+
return invalid("login_style styles the Application Portal login page; declare portal as well, or remove login_style.");
|
|
309
|
+
const unknown = Object.keys(value).filter(key => !LOGIN_STYLE_FIELDS.includes(key));
|
|
310
|
+
if (unknown.length)
|
|
311
|
+
return invalid(`login_style does not accept ${unknown.join(", ")}; use ${LOGIN_STYLE_FIELDS.join(", ")}.`);
|
|
312
|
+
if (value.color !== undefined && (typeof value.color !== "string" || !LOGIN_COLOR.test(value.color))) {
|
|
313
|
+
return invalid('login_style.color must be a hex colour such as "#0F766E".');
|
|
314
|
+
}
|
|
315
|
+
if ((value.wallpaper === undefined) !== (value.wallpaper_thumbnail === undefined)) {
|
|
316
|
+
return invalid("login_style.wallpaper and login_style.wallpaper_thumbnail go together: DSM uses a package wallpaper only when both are set.");
|
|
317
|
+
}
|
|
318
|
+
const image = (field) => {
|
|
319
|
+
if (value[field] === undefined)
|
|
320
|
+
return undefined;
|
|
321
|
+
const path = c.path(value[field], `login_style.${field}`);
|
|
322
|
+
const problem = path && loginImageProblem(field, path);
|
|
323
|
+
return problem ? c.fail("STANDALONE_LOGIN_STYLE_IMAGE_INVALID", `login_style.${field}: ${problem}`) : path;
|
|
324
|
+
};
|
|
325
|
+
const [wallpaper, thumbnail, title] = [image("wallpaper"), image("wallpaper_thumbnail"), image("title")];
|
|
326
|
+
return {
|
|
327
|
+
...(value.color !== undefined ? { color: value.color } : {}),
|
|
328
|
+
...(wallpaper && thumbnail ? { wallpaper: { image: wallpaper, thumbnail } } : {}),
|
|
329
|
+
...(title ? { title } : {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
284
332
|
/** Where each backend kind's CGI main wraps its authenticator in the SDK entry gate. */
|
|
285
333
|
const ENTRY_GATE = {
|
|
286
334
|
go: { main: b => b.kind === "go" ? join(b.dir, b.main, "main.go") : "", call: "dsm.NewEntryGate(", sdk: `backend/go.mod must require ${SDK_MODULE}` },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/** The panel text by locale, keyed without the SDK's `portal.` prefix. */
|
|
2
|
+
export type PortalCatalogs = Record<string, Record<string, string>>;
|
|
3
|
+
/** The SDK's portal panel text (`ts/i18n/portal/<locale>.json` of the App's SDK), which the SDK owns and its Vue panel shows too. */
|
|
4
|
+
export declare function portalCatalogs(sdkDir: string): PortalCatalogs;
|
|
4
5
|
/**
|
|
5
6
|
* `createPortalAccess(options)` → `ctx.portal`, plain ES5 for DSM's desktop.
|
|
6
7
|
*
|
|
@@ -16,8 +17,7 @@ export declare const PORTAL_MESSAGES: Record<string, Record<PortalLocale, string
|
|
|
16
17
|
* status or reject `{code}`. With `options.entry` (below DSM 7.2) the
|
|
17
18
|
* backend's `portal.status` also reports the executor's entry, and
|
|
18
19
|
* administrators get `register()` and `unregister()`; `openControlPanel()` opens Login Portal > Applications.
|
|
19
|
-
* `messages` is the panel text in the window's locale; `mountPanel(element)`
|
|
20
|
+
* `messages` is the SDK's panel text in the window's locale; `mountPanel(element)`
|
|
20
21
|
* renders a plain panel for UIs without their own.
|
|
21
22
|
*/
|
|
22
|
-
export declare const
|
|
23
|
-
export {};
|
|
23
|
+
export declare const portalRuntime: (catalogs: PortalCatalogs) => string;
|
|
@@ -1,33 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { PORTAL_ALIAS_PATTERN, RESERVED_PORTAL_ALIASES } from "../portal/alias.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
apply: { en: "Apply", "zh-TW": "套用", "zh-CN": "应用", ja: "適用", ko: "적용", de: "Übernehmen", fr: "Appliquer", es: "Aplicar", it: "Applica" },
|
|
10
|
-
disable: { en: "Turn off", "zh-TW": "停用", "zh-CN": "停用", ja: "無効にする", ko: "끄기", de: "Ausschalten", fr: "Désactiver", es: "Desactivar", it: "Disattiva" },
|
|
11
|
-
advanced: { en: "Advanced settings in Control Panel", "zh-TW": "在控制台進行進階設定", "zh-CN": "在控制面板中进行高级设置", ja: "コントロールパネルの詳細設定", ko: "제어판의 고급 설정", de: "Erweiterte Einstellungen in der Systemsteuerung", fr: "Paramètres avancés dans le Panneau de configuration", es: "Ajustes avanzados en el Panel de control", it: "Impostazioni avanzate nel Pannello di controllo" },
|
|
12
|
-
controlPanel: { en: "Open Control Panel Application Portal", "zh-TW": "開啟控制台 Application Portal", "zh-CN": "打开控制面板 Application Portal", ja: "コントロールパネルの Application Portal を開く", ko: "제어판 Application Portal 열기", de: "Application Portal in der Systemsteuerung öffnen", fr: "Ouvrir Application Portal dans le Panneau de configuration", es: "Abrir Application Portal en el Panel de control", it: "Apri Application Portal nel Pannello di controllo" },
|
|
13
|
-
entryOff: { en: "This app has no Application Portal entry yet. Enable the entry so that an alias can open it after DSM sign-in.", "zh-TW": "這個 App 尚未加入應用程式入口。啟用入口後,即可用別名在 DSM 登入後直接開啟。", "zh-CN": "此应用尚未加入应用程序门户。启用入口后,即可用别名在 DSM 登录后直接打开。", ja: "このアプリはまだアプリケーションポータルに登録されていません。エントリを有効にすると、DSM サインイン後にエイリアスで直接開けます。", ko: "이 앱은 아직 응용 프로그램 포털에 등록되지 않았습니다. 항목을 활성화하면 DSM 로그인 후 별칭으로 바로 열 수 있습니다.", de: "Diese App hat noch keinen Eintrag im Anwendungsportal. Aktivieren Sie den Eintrag, damit ein Alias sie nach der DSM-Anmeldung öffnet.", fr: "Cette application n'a pas encore d'entrée dans le Portail d'application. Activez l'entrée pour qu'un alias l'ouvre après connexion à DSM.", es: "Esta aplicación aún no tiene una entrada en el Portal de aplicaciones. Active la entrada para que un alias la abra tras iniciar sesión en DSM.", it: "Questa app non ha ancora una voce nel Portale applicazioni. Attivare la voce per aprirla con un alias dopo l'accesso a DSM." },
|
|
14
|
-
cleanupRequired: { en: "The Application Portal entry was not fully removed. Remove the entry again.", "zh-TW": "應用程式入口未完全移除。請再移除一次入口。", "zh-CN": "应用程序门户入口未完全移除。请再移除一次入口。", ja: "アプリケーションポータルのエントリが完全に削除されていません。もう一度エントリを削除してください。", ko: "응용 프로그램 포털 항목이 완전히 제거되지 않았습니다. 항목을 다시 제거하십시오.", de: "Der Eintrag im Anwendungsportal wurde nicht vollständig entfernt. Entfernen Sie den Eintrag erneut.", fr: "L'entrée du Portail d'application n'a pas été entièrement supprimée. Supprimez de nouveau l'entrée.", es: "La entrada del Portal de aplicaciones no se eliminó por completo. Elimine la entrada de nuevo.", it: "La voce del Portale applicazioni non è stata rimossa completamente. Rimuovere di nuovo la voce." },
|
|
15
|
-
register: { en: "Enable entry", "zh-TW": "啟用入口", "zh-CN": "启用入口", ja: "エントリを有効にする", ko: "항목 활성화", de: "Eintrag aktivieren", fr: "Activer l'entrée", es: "Activar entrada", it: "Attiva voce" },
|
|
16
|
-
unregister: { en: "Remove entry", "zh-TW": "移除入口", "zh-CN": "移除入口", ja: "エントリを削除", ko: "항목 제거", de: "Eintrag entfernen", fr: "Supprimer l'entrée", es: "Eliminar entrada", it: "Rimuovi voce" },
|
|
17
|
-
portal_alias_invalid: { en: "Use 1-64 lowercase letters, digits or hyphens, not starting or ending with a hyphen.", "zh-TW": "請使用 1–64 個小寫英文字母、數字或連字號,且不可以連字號開頭或結尾。", "zh-CN": "请使用 1–64 个小写英文字母、数字或连字符,且不能以连字符开头或结尾。", ja: "1~64 文字の小文字英字・数字・ハイフンを使用し、先頭と末尾にハイフンは使えません。", ko: "1~64자의 영문 소문자, 숫자 또는 하이픈을 사용하고 하이픈으로 시작하거나 끝나지 않게 하십시오.", de: "Verwenden Sie 1–64 Kleinbuchstaben, Ziffern oder Bindestriche, ohne Bindestrich am Anfang oder Ende.", fr: "Utilisez 1 à 64 lettres minuscules, chiffres ou tirets, sans tiret au début ni à la fin.", es: "Use de 1 a 64 letras minúsculas, dígitos o guiones, sin guion al principio ni al final.", it: "Usare da 1 a 64 lettere minuscole, cifre o trattini, senza trattino all'inizio o alla fine." },
|
|
18
|
-
portal_alias_reserved: { en: "DSM already uses this alias. Choose another one.", "zh-TW": "DSM 已使用這個別名,請改用其他別名。", "zh-CN": "DSM 已使用此别名,请改用其他别名。", ja: "このエイリアスは DSM で使用されています。別のものを選んでください。", ko: "DSM에서 이미 사용하는 별칭입니다. 다른 별칭을 선택하십시오.", de: "DSM verwendet diesen Alias bereits. Wählen Sie einen anderen.", fr: "DSM utilise déjà cet alias. Choisissez-en un autre.", es: "DSM ya usa este alias. Elija otro.", it: "DSM usa già questo alias. Sceglierne un altro." },
|
|
19
|
-
portal_alias_in_use: { en: "Another app already uses this alias. Choose another one.", "zh-TW": "另一個 App 已使用這個別名,請改用其他別名。", "zh-CN": "另一个应用已使用此别名,请改用其他别名。", ja: "別のアプリがこのエイリアスを使用しています。別のものを選んでください。", ko: "다른 앱이 이미 이 별칭을 사용합니다. 다른 별칭을 선택하십시오.", de: "Eine andere App verwendet diesen Alias bereits. Wählen Sie einen anderen.", fr: "Une autre application utilise déjà cet alias. Choisissez-en un autre.", es: "Otra aplicación ya usa este alias. Elija otro.", it: "Un'altra app usa già questo alias. Sceglierne un altro." },
|
|
20
|
-
admin_required: { en: "Only a DSM administrator can change this.", "zh-TW": "只有 DSM 管理員可以變更這項設定。", "zh-CN": "只有 DSM 管理员可以更改此设置。", ja: "これを変更できるのは DSM 管理者のみです。", ko: "DSM 관리자만 변경할 수 있습니다.", de: "Nur ein DSM-Administrator kann dies ändern.", fr: "Seul un administrateur DSM peut modifier ce paramètre.", es: "Solo un administrador de DSM puede cambiar esto.", it: "Solo un amministratore DSM può modificarlo." },
|
|
21
|
-
portal_unavailable: { en: "DSM did not accept the change. Try again or use Control Panel.", "zh-TW": "DSM 未接受變更。請再試一次或改用控制台。", "zh-CN": "DSM 未接受更改。请重试或改用控制面板。", ja: "DSM は変更を受け付けませんでした。再試行するか、コントロールパネルを使用してください。", ko: "DSM이 변경을 받아들이지 않았습니다. 다시 시도하거나 제어판을 사용하십시오.", de: "DSM hat die Änderung nicht übernommen. Versuchen Sie es erneut oder verwenden Sie die Systemsteuerung.", fr: "DSM n'a pas accepté la modification. Réessayez ou utilisez le Panneau de configuration.", es: "DSM no aceptó el cambio. Inténtelo de nuevo o use el Panel de control.", it: "DSM non ha accettato la modifica. Riprovare o usare il Pannello di controllo." },
|
|
22
|
-
PORTAL_FAILED: { en: "DSM did not accept the Application Portal change. Try again.", "zh-TW": "DSM 未接受應用程式入口的變更。請再試一次。", "zh-CN": "DSM 未接受应用程序门户的更改。请重试。", ja: "DSM はアプリケーションポータルの変更を受け付けませんでした。再試行してください。", ko: "DSM이 응용 프로그램 포털 변경을 받아들이지 않았습니다. 다시 시도하십시오.", de: "DSM hat die Änderung am Anwendungsportal nicht übernommen. Versuchen Sie es erneut.", fr: "DSM n'a pas accepté la modification du Portail d'application. Réessayez.", es: "DSM no aceptó el cambio en el Portal de aplicaciones. Inténtelo de nuevo.", it: "DSM non ha accettato la modifica al Portale applicazioni. Riprovare." },
|
|
23
|
-
};
|
|
24
|
-
function localizedMessages() {
|
|
25
|
-
const out = {};
|
|
26
|
-
for (const [key, texts] of Object.entries(PORTAL_MESSAGES)) {
|
|
27
|
-
for (const [locale, text] of Object.entries(texts))
|
|
28
|
-
(out[locale] ??= {})[key] = text;
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
4
|
+
import { LOCALES } from "./i18n.js";
|
|
5
|
+
/** The SDK's portal panel text (`ts/i18n/portal/<locale>.json` of the App's SDK), which the SDK owns and its Vue panel shows too. */
|
|
6
|
+
export function portalCatalogs(sdkDir) {
|
|
7
|
+
return Object.fromEntries(LOCALES.map((locale) => {
|
|
8
|
+
const catalog = JSON.parse(readFileSync(join(sdkDir, "ts/i18n/portal", `${locale}.json`), "utf8"));
|
|
9
|
+
return [locale, Object.fromEntries(Object.entries(catalog).map(([key, text]) => [key.slice("portal.".length), text]))];
|
|
10
|
+
}));
|
|
31
11
|
}
|
|
32
12
|
/**
|
|
33
13
|
* `createPortalAccess(options)` → `ctx.portal`, plain ES5 for DSM's desktop.
|
|
@@ -44,14 +24,14 @@ function localizedMessages() {
|
|
|
44
24
|
* status or reject `{code}`. With `options.entry` (below DSM 7.2) the
|
|
45
25
|
* backend's `portal.status` also reports the executor's entry, and
|
|
46
26
|
* administrators get `register()` and `unregister()`; `openControlPanel()` opens Login Portal > Applications.
|
|
47
|
-
* `messages` is the panel text in the window's locale; `mountPanel(element)`
|
|
27
|
+
* `messages` is the SDK's panel text in the window's locale; `mountPanel(element)`
|
|
48
28
|
* renders a plain panel for UIs without their own.
|
|
49
29
|
*/
|
|
50
|
-
export const
|
|
30
|
+
export const portalRuntime = (catalogs) => `function createPortalAccess(options) {
|
|
51
31
|
var ALIAS = new RegExp(${JSON.stringify(PORTAL_ALIAS_PATTERN.source)});
|
|
52
32
|
var RESERVED = ${JSON.stringify(RESERVED_PORTAL_ALIASES)};
|
|
53
33
|
var LINKABLE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
54
|
-
var CATALOGS = ${JSON.stringify(
|
|
34
|
+
var CATALOGS = ${JSON.stringify(catalogs)};
|
|
55
35
|
var messages = CATALOGS[options.locale] || CATALOGS.en;
|
|
56
36
|
var ENTRIES = ["registered", "unregistered", "cleanup-required"];
|
|
57
37
|
var ERRORS = { dsm_4100: "portal_alias_invalid", dsm_4102: "portal_alias_reserved", dsm_4103: "portal_alias_in_use", dsm_105: "admin_required" };
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
/** The backend's request deadline in seconds unless the App sets backend.timeout_seconds. */
|
|
2
|
-
export declare const DEFAULT_REQUEST_TIMEOUT_SECONDS = 60;
|
|
3
1
|
/** spec/bridge/v1 timeout_seconds: the runtime refuses more. */
|
|
4
2
|
export declare const MAX_REQUEST_TIMEOUT_SECONDS = 3600;
|
|
5
|
-
export declare const TIMEOUT_USAGE = "timeout_seconds is the request deadline in seconds, a whole number from 1 to 3600; omitted, 60.";
|
|
3
|
+
export declare const TIMEOUT_USAGE = "timeout_seconds is the request deadline in seconds, a whole number from 1 to 3600; omitted, the request default of the App's SDK (60 seconds through SDK 0.10).";
|
|
6
4
|
/** Whether backend.timeout_seconds is a deadline the runtime accepts. */
|
|
7
5
|
export declare function validTimeoutSeconds(value: unknown): value is number;
|
|
8
6
|
/** The request deadline the CGI applies and the window waits past. */
|
|
9
7
|
export declare function requestTimeoutSeconds(backend: object): number;
|
|
8
|
+
/** Why a request deadline ends before a program root.run may run would answer, marginSeconds after its timeout_s; undefined when it does not. */
|
|
9
|
+
export declare function rootRunDeadlineProblem(programs: ReadonlyArray<{
|
|
10
|
+
name: string;
|
|
11
|
+
timeoutSeconds: number;
|
|
12
|
+
}>, requestSeconds: number, marginSeconds: number): string | undefined;
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
export const DEFAULT_REQUEST_TIMEOUT_SECONDS = 60;
|
|
1
|
+
import { appSdkTimeouts } from "../runtime/sdk-timeouts.js";
|
|
3
2
|
/** spec/bridge/v1 timeout_seconds: the runtime refuses more. */
|
|
4
3
|
export const MAX_REQUEST_TIMEOUT_SECONDS = 3600;
|
|
5
|
-
export const TIMEOUT_USAGE = `timeout_seconds is the request deadline in seconds, a whole number from 1 to ${MAX_REQUEST_TIMEOUT_SECONDS}; omitted,
|
|
4
|
+
export const TIMEOUT_USAGE = `timeout_seconds is the request deadline in seconds, a whole number from 1 to ${MAX_REQUEST_TIMEOUT_SECONDS}; omitted, the request default of the App's SDK (60 seconds through SDK 0.10).`;
|
|
6
5
|
/** Whether backend.timeout_seconds is a deadline the runtime accepts. */
|
|
7
6
|
export function validTimeoutSeconds(value) {
|
|
8
7
|
return Number.isInteger(value) && value >= 1 && value <= MAX_REQUEST_TIMEOUT_SECONDS;
|
|
9
8
|
}
|
|
10
9
|
/** The request deadline the CGI applies and the window waits past. */
|
|
11
10
|
export function requestTimeoutSeconds(backend) {
|
|
12
|
-
return backend.timeoutSeconds ??
|
|
11
|
+
return backend.timeoutSeconds ?? appSdkTimeouts(backend).requestDefaultSeconds;
|
|
12
|
+
}
|
|
13
|
+
/** Why a request deadline ends before a program root.run may run would answer, marginSeconds after its timeout_s; undefined when it does not. */
|
|
14
|
+
export function rootRunDeadlineProblem(programs, requestSeconds, marginSeconds) {
|
|
15
|
+
const short = programs.find(p => p.timeoutSeconds + marginSeconds > requestSeconds);
|
|
16
|
+
if (!short)
|
|
17
|
+
return undefined;
|
|
18
|
+
return `backend.timeout_seconds is ${requestSeconds}, but root.run waits for root program ${short.name} up to its run.timeout_s ${short.timeoutSeconds} + ${marginSeconds} seconds; set timeout_seconds to at least ${short.timeoutSeconds + marginSeconds}, or lower run.timeout_s.`;
|
|
13
19
|
}
|