@synopackageland/cli 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +1 -1
  3. package/assets/licenses/MUSL-COPYRIGHT +193 -0
  4. package/assets/licenses/RUST-COPYRIGHT-library.html +5556 -0
  5. package/dist/add.js +29 -29
  6. package/dist/build-spk/dsm-version.d.ts +14 -4
  7. package/dist/build-spk/dsm-version.js +17 -5
  8. package/dist/build-spk/notices.d.ts +8 -1
  9. package/dist/build-spk/notices.js +13 -4
  10. package/dist/build-spk/uninstall-wizard.d.ts +11 -0
  11. package/dist/build-spk/uninstall-wizard.js +16 -3
  12. package/dist/build-spk/wizard-i18n.d.ts +1 -1
  13. package/dist/build-spk/wizard-i18n.js +1 -1
  14. package/dist/build-spk/wizard-messages.d.ts +44 -0
  15. package/dist/build-spk/wizard-messages.js +44 -0
  16. package/dist/capability-overlays.d.ts +10 -5
  17. package/dist/capability-overlays.js +32 -10
  18. package/dist/cli.js +5 -5
  19. package/dist/deploy/plan.d.ts +1 -1
  20. package/dist/deploy/plan.js +1 -1
  21. package/dist/error-registry.js +46 -4
  22. package/dist/info.js +4 -3
  23. package/dist/init.js +53 -28
  24. package/dist/portal/alias.d.ts +8 -0
  25. package/dist/portal/alias.js +16 -0
  26. package/dist/runtime/executor-build.d.ts +1 -1
  27. package/dist/runtime/executor-build.js +2 -1
  28. package/dist/runtime/executor-digests.json +22 -0
  29. package/dist/runtime/executor-policy.d.ts +15 -0
  30. package/dist/runtime/executor-policy.js +32 -6
  31. package/dist/runtime/sdk-source.d.ts +14 -2
  32. package/dist/runtime/sdk-source.js +39 -4
  33. package/dist/runtime/uninstall-cleanup.d.ts +5 -1
  34. package/dist/runtime/uninstall-cleanup.js +11 -1
  35. package/dist/standalone/api-permissions.d.ts +4 -2
  36. package/dist/standalone/api-permissions.js +6 -5
  37. package/dist/standalone/binary.d.ts +3 -2
  38. package/dist/standalone/binary.js +17 -5
  39. package/dist/standalone/build.js +16 -12
  40. package/dist/standalone/daemon.d.ts +6 -0
  41. package/dist/standalone/daemon.js +8 -4
  42. package/dist/standalone/dsm-window.js +16 -3
  43. package/dist/standalone/inspect-spk.d.ts +1 -1
  44. package/dist/standalone/inspect-spk.js +46 -13
  45. package/dist/standalone/lifecycle.js +38 -4
  46. package/dist/standalone/manifest.d.ts +12 -0
  47. package/dist/standalone/manifest.js +62 -20
  48. package/dist/standalone/native-layout.d.ts +4 -7
  49. package/dist/standalone/native-layout.js +41 -102
  50. package/dist/standalone/native-root-window.d.ts +2 -1
  51. package/dist/standalone/native-root-window.js +15 -5
  52. package/dist/standalone/portal-window.d.ts +4 -2
  53. package/dist/standalone/portal-window.js +54 -12
  54. package/dist/standalone/portal.d.ts +4 -1
  55. package/dist/standalone/portal.js +8 -2
  56. package/dist/standalone/root-ops.d.ts +2 -0
  57. package/dist/standalone/root-ops.js +2 -0
  58. package/dist/standalone/rust-build.d.ts +35 -0
  59. package/dist/standalone/rust-build.js +127 -0
  60. package/dist/standalone/sdk-check.d.ts +5 -3
  61. package/dist/standalone/sdk-check.js +21 -8
  62. package/dist/standalone/spk.js +15 -3
  63. package/dist/standalone/toolkit.d.ts +10 -0
  64. package/dist/standalone/toolkit.js +101 -0
  65. package/dist/standalone/uninstall-hook.d.ts +10 -0
  66. package/dist/standalone/uninstall-hook.js +71 -0
  67. package/dist/standalone/uninstall.d.ts +53 -0
  68. package/dist/standalone/uninstall.js +136 -0
  69. package/dist/types.d.ts +1 -1
  70. package/dist/yaml-edit.d.ts +6 -0
  71. package/dist/yaml-edit.js +31 -0
  72. package/package.json +2 -2
  73. package/skills/reference/portal.md +8 -4
@@ -4,17 +4,22 @@ export const PORTAL_MESSAGES = {
4
4
  title: { en: "Application Portal", "zh-TW": "應用程式入口", "zh-CN": "应用程序门户", ja: "アプリケーションポータル", ko: "응용 프로그램 포털", de: "Anwendungsportal", fr: "Portail d'application", es: "Portal de aplicaciones", it: "Portale applicazioni" },
5
5
  link: { en: "Direct link after DSM sign-in:", "zh-TW": "DSM 登入後的直接連結:", "zh-CN": "DSM 登录后的直接链接:", ja: "DSM サインイン後の直接リンク:", ko: "DSM 로그인 후 바로 가기 링크:", de: "Direkter Link nach der DSM-Anmeldung:", fr: "Lien direct après connexion à DSM :", es: "Enlace directo tras iniciar sesión en DSM:", it: "Link diretto dopo l'accesso a DSM:" },
6
6
  inactive: { en: "Not active. Apply an alias to let users open this app directly after DSM sign-in.", "zh-TW": "尚未啟用。套用別名後,使用者可在 DSM 登入後直接開啟這個 App。", "zh-CN": "尚未启用。应用别名后,用户可在 DSM 登录后直接打开此应用。", ja: "無効です。エイリアスを適用すると、DSM サインイン後にこのアプリを直接開けます。", ko: "비활성 상태입니다. 별칭을 적용하면 DSM 로그인 후 이 앱을 바로 열 수 있습니다.", de: "Nicht aktiv. Wenden Sie einen Alias an, damit Benutzer diese App nach der DSM-Anmeldung direkt öffnen können.", fr: "Inactif. Appliquez un alias pour ouvrir cette application directement après connexion à DSM.", es: "Inactivo. Aplique un alias para abrir esta aplicación directamente tras iniciar sesión en DSM.", it: "Non attivo. Applica un alias per aprire questa app direttamente dopo l'accesso a DSM." },
7
- askAdmin: { en: "Ask an administrator to set this up", "zh-TW": "請管理員設定", "zh-CN": "请管理员设置", ja: "管理者に設定を依頼してください", ko: "관리자에게 설정을 요청하세요", de: "Bitten Sie einen Administrator, dies einzurichten", fr: "Demandez à un administrateur de le configurer", es: "Pida a un administrador que lo configure", it: "Chiedi a un amministratore di configurarlo" },
7
+ askAdmin: { en: "Ask an administrator to set this up", "zh-TW": "請管理員設定", "zh-CN": "请管理员设置", ja: "管理者に設定を依頼してください", ko: "관리자에게 설정을 요청하십시오", de: "Bitten Sie einen Administrator, dies einzurichten", fr: "Demandez à un administrateur de le configurer", es: "Pida a un administrador que lo configure", it: "Chiedi a un amministratore di configurarlo" },
8
8
  alias: { en: "Alias", "zh-TW": "別名", "zh-CN": "别名", ja: "エイリアス", ko: "별칭", de: "Alias", fr: "Alias", es: "Alias", it: "Alias" },
9
9
  apply: { en: "Apply", "zh-TW": "套用", "zh-CN": "应用", ja: "適用", ko: "적용", de: "Übernehmen", fr: "Appliquer", es: "Aplicar", it: "Applica" },
10
10
  disable: { en: "Turn off", "zh-TW": "停用", "zh-CN": "停用", ja: "無効にする", ko: "끄기", de: "Ausschalten", fr: "Désactiver", es: "Desactivar", it: "Disattiva" },
11
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
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
- 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: "Usa da 1 a 64 lettere minuscole, cifre o trattini, senza trattino all'inizio o alla fine." },
14
- 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. Scegline un altro." },
15
- 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. Scegline un altro." },
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: "Usa 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. Scegline 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. Scegline un altro." },
16
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." },
17
- 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. Riprova o usa il Pannello di controllo." },
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. Riprova o usa 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." },
18
23
  };
19
24
  function localizedMessages() {
20
25
  const out = {};
@@ -35,8 +40,10 @@ function localizedMessages() {
35
40
  * Control Panel shortcut.
36
41
  *
37
42
  * `status()` resolves `{state, alias?, url?, canManage, controlPanel,
38
- * defaultAlias}`; `apply(alias)` and `disable()` resolve the new status or
39
- * reject `{code}`; `openControlPanel()` opens Login Portal > Applications.
43
+ * defaultAlias, entry?}`; `apply(alias)` and `disable()` resolve the new
44
+ * status or reject `{code}`. With `options.entry` (below DSM 7.2) the
45
+ * backend's `portal.status` also reports the executor's entry, and
46
+ * administrators get `register()` and `unregister()`; `openControlPanel()` opens Login Portal > Applications.
40
47
  * `messages` is the panel text in the window's locale; `mountPanel(element)`
41
48
  * renders a plain panel for UIs without their own.
42
49
  */
@@ -46,6 +53,7 @@ export const PORTAL_RUNTIME = `function createPortalAccess(options) {
46
53
  var LINKABLE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
47
54
  var CATALOGS = ${JSON.stringify(localizedMessages())};
48
55
  var messages = CATALOGS[options.locale] || CATALOGS.en;
56
+ var ENTRIES = ["registered", "unregistered", "cleanup-required"];
49
57
  var ERRORS = { dsm_4100: "portal_alias_invalid", dsm_4102: "portal_alias_reserved", dsm_4103: "portal_alias_in_use", dsm_105: "admin_required" };
50
58
  function result(state, alias, managed) {
51
59
  var out = { state: state, canManage: managed === true, controlPanel: managed !== undefined, defaultAlias: options.defaultAlias };
@@ -55,11 +63,12 @@ export const PORTAL_RUNTIME = `function createPortalAccess(options) {
55
63
  }
56
64
  return out;
57
65
  }
66
+ function readBackend(data) {
67
+ if (data && data.state === "active" && typeof data.alias === "string" && LINKABLE.test(data.alias)) return result("active", data.alias);
68
+ return result(data && data.state === "inactive" ? "inactive" : "unavailable");
69
+ }
58
70
  function fromBackend() {
59
- return options.call("portal.status", {}).then(function (data) {
60
- if (data && data.state === "active" && typeof data.alias === "string" && LINKABLE.test(data.alias)) return result("active", data.alias);
61
- return result(data && data.state === "inactive" ? "inactive" : "unavailable");
62
- }, function () {
71
+ return options.call("portal.status", {}).then(readBackend, function () {
63
72
  return result("unavailable");
64
73
  });
65
74
  }
@@ -79,9 +88,33 @@ export const PORTAL_RUNTIME = `function createPortalAccess(options) {
79
88
  return result("unavailable", undefined, false);
80
89
  });
81
90
  }
91
+ function withEntry(out, entry) {
92
+ out.entry = entry;
93
+ return out;
94
+ }
95
+ // Below DSM 7.2 the executor registers the entry; DSM lists the App only while it is registered.
96
+ function fromEntry() {
97
+ return options.call("portal.status", {}).then(function (data) {
98
+ var entry = data && ENTRIES.indexOf(data.entry) >= 0 ? data.entry : "";
99
+ if (!options.isAdmin()) return readBackend(data);
100
+ if (entry === "registered") return fromDsm().then(function (out) { return withEntry(out, entry); });
101
+ if (!entry) return result("unavailable", undefined, false);
102
+ var read = readBackend(data);
103
+ return withEntry(result(entry === "unregistered" ? "inactive" : read.state, read.alias, true), entry);
104
+ }, function () {
105
+ return options.isAdmin() ? result("unavailable", undefined, false) : result("unavailable");
106
+ });
107
+ }
82
108
  function status() {
109
+ if (options.entry) return fromEntry();
83
110
  return options.isAdmin() ? fromDsm() : fromBackend();
84
111
  }
112
+ function change(method) {
113
+ if (!options.isAdmin()) return Promise.reject({ code: "admin_required" });
114
+ return options.call(method, {}).then(status, function (error) {
115
+ throw { code: error && error.code === "ADMIN_REQUIRED" ? "admin_required" : "PORTAL_FAILED" };
116
+ });
117
+ }
85
118
  function set(alias) {
86
119
  if (!options.isAdmin()) return Promise.reject({ code: "admin_required" });
87
120
  return options.send("SYNO.Core.AppPortal", "set", 2, { id: options.appId, alias: alias }).then(status, function (error) {
@@ -106,6 +139,10 @@ export const PORTAL_RUNTIME = `function createPortalAccess(options) {
106
139
  return mountPanel(element);
107
140
  }
108
141
  };
142
+ if (options.entry) {
143
+ access.register = function () { return change("portal.register"); };
144
+ access.unregister = function () { return change("portal.unregister"); };
145
+ }
109
146
  function node(tag, attrs, children) {
110
147
  var out = document.createElement(tag);
111
148
  for (var key in attrs) {
@@ -129,7 +166,11 @@ export const PORTAL_RUNTIME = `function createPortalAccess(options) {
129
166
  if (current.state === "active") {
130
167
  parts.push(node("p", {}, [node("span", { text: messages.link + " " }), node("a", { href: current.url, target: "_blank", rel: "noopener", text: current.url })]));
131
168
  }
132
- if (current.canManage) {
169
+ if (current.canManage && current.entry && current.entry !== "registered") {
170
+ var off = current.entry === "unregistered";
171
+ parts.push(node("p", { text: off ? messages.entryOff : messages.cleanupRequired }));
172
+ parts.push(off ? button(messages.register, function () { settle(access.register()); }) : button(messages.unregister, function () { settle(access.unregister()); }));
173
+ } else if (current.canManage) {
133
174
  if (current.state !== "active") parts.push(node("p", { text: messages.inactive }));
134
175
  var input = node("input", { type: "text", maxlength: "64", autocomplete: "off", "aria-label": messages.alias });
135
176
  input.value = current.alias || current.defaultAlias;
@@ -137,6 +178,7 @@ export const PORTAL_RUNTIME = `function createPortalAccess(options) {
137
178
  form.addEventListener("submit", function (event) { event.preventDefault(); settle(access.apply(input.value)); });
138
179
  if (current.state === "active") form.appendChild(button(messages.disable, function () { settle(access.disable()); }));
139
180
  parts.push(form, button(messages.advanced, access.openControlPanel));
181
+ if (current.entry) parts.push(button(messages.unregister, function () { settle(access.unregister()); }));
140
182
  } else if (current.controlPanel) {
141
183
  parts.push(button(messages.controlPanel, access.openControlPanel));
142
184
  } else if (current.state !== "active") {
@@ -1,4 +1,7 @@
1
1
  import type { StandaloneManifest } from "./manifest.js";
2
2
  import type { PortalPackage } from "../portal/package.js";
3
- /** A native portal opens the package's own DSM window. */
3
+ /**
4
+ * A native portal registered through DSM's web-config resource; it opens the
5
+ * package's own DSM window. Below DSM 7.2 the executor registers it instead.
6
+ */
4
7
  export declare function nativePortal(manifest: StandaloneManifest): PortalPackage | undefined;
@@ -1,4 +1,10 @@
1
- /** A native portal opens the package's own DSM window. */
1
+ import { portalByExecutor } from "../build-spk/dsm-version.js";
2
+ /**
3
+ * A native portal registered through DSM's web-config resource; it opens the
4
+ * package's own DSM window. Below DSM 7.2 the executor registers it instead.
5
+ */
2
6
  export function nativePortal(manifest) {
3
- return manifest.portal && { appId: `SYNO.SDS.${manifest.package}.Instance`, application: manifest.displayname, alias: manifest.portal.alias };
7
+ if (!manifest.portal || portalByExecutor(manifest.os_min_ver))
8
+ return undefined;
9
+ return { appId: `SYNO.SDS.${manifest.package}.Instance`, application: manifest.displayname, alias: manifest.portal.alias };
4
10
  }
@@ -12,4 +12,6 @@ export type RootOpsResult = {
12
12
  errors: StandaloneError[];
13
13
  };
14
14
  export declare const ROOT_OPS_DIR = "root";
15
+ /** Shell that tries to become root: sudo, chown root or setuid/setgid modes. */
16
+ export declare const ROOT_SCRIPT_HINTS: RegExp[];
15
17
  export declare function loadRootOps(dir: string): RootOpsResult;
@@ -1,6 +1,8 @@
1
1
  import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  export const ROOT_OPS_DIR = "root";
4
+ /** Shell that tries to become root: sudo, chown root or setuid/setgid modes. */
5
+ export const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
4
6
  const MAX_ROOT_OP_BYTES = 16 * 1024;
5
7
  const OP_FILE = /^([a-z][a-z0-9_]{0,31})\.sh$/;
6
8
  export function loadRootOps(dir) {
@@ -0,0 +1,35 @@
1
+ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
+ import type { CommandRunner } from "./build.js";
3
+ import type { StandaloneManifest } from "./manifest.js";
4
+ import { type Toolkit } from "./toolkit.js";
5
+ /** The glibc loader of DSM's ARMv7 models, the one interpreter an armv7 Rust CGI may name. */
6
+ export declare const ARMV7_INTERPRETER = "/lib/ld-linux-armhf.so.3";
7
+ interface RustTarget {
8
+ triple: string;
9
+ /** The linker, given the extracted toolkit folder when the target pins one. */
10
+ linker: (toolkit: string) => string;
11
+ linkArgs: string[];
12
+ toolkit?: Toolkit;
13
+ interpreter?: string;
14
+ }
15
+ /**
16
+ * x86_64 and aarch64 link static musl binaries with rust-lld. armv7 links
17
+ * against the glibc of the DSM 7.0 toolkit, because musl's 64-bit time calls
18
+ * fail on DSM's ARMv7 kernels; that gcc runs on Linux x86_64 only.
19
+ */
20
+ export declare const RUST_TARGETS: Record<NativeArchFamily, RustTarget>;
21
+ /** Go's `-X` values for a Rust App, read with `option_env!`; every one is set, empty when unused. */
22
+ export declare function rustBuildEnv(manifest: StandaloneManifest): Record<string, string>;
23
+ export interface RustBuildDeps {
24
+ host?: {
25
+ platform: string;
26
+ arch: string;
27
+ };
28
+ prepareToolkit?: (cwd: string, arch: string, inputs: Toolkit[], run: CommandRunner) => Promise<string>;
29
+ }
30
+ /** Builds the CGI bin, and the worker bin with capabilities.jobs, in one `cargo build` into `work`. */
31
+ export declare function buildRustBackend(manifest: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner, { host, prepareToolkit: prepare }?: RustBuildDeps): Promise<{
32
+ cgi: string;
33
+ worker?: string;
34
+ }>;
35
+ export {};
@@ -0,0 +1,127 @@
1
+ import { copyFileSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { portalByExecutor } from "../build-spk/dsm-version.js";
5
+ import { SDK_GIT_URL } from "../runtime/sdk-source.js";
6
+ import { checkCgiBinary } from "./binary.js";
7
+ import { StandaloneBuildError } from "./spk.js";
8
+ import { prepareToolkit } from "./toolkit.js";
9
+ /** The glibc loader of DSM's ARMv7 models, the one interpreter an armv7 Rust CGI may name. */
10
+ export const ARMV7_INTERPRETER = "/lib/ld-linux-armhf.so.3";
11
+ const ARMV7_GCC = "usr/local/arm-unknown-linux-gnueabi/bin/arm-unknown-linux-gnueabi-gcc";
12
+ /**
13
+ * x86_64 and aarch64 link static musl binaries with rust-lld. armv7 links
14
+ * against the glibc of the DSM 7.0 toolkit, because musl's 64-bit time calls
15
+ * fail on DSM's ARMv7 kernels; that gcc runs on Linux x86_64 only.
16
+ */
17
+ export const RUST_TARGETS = {
18
+ x86_64: { triple: "x86_64-unknown-linux-musl", linker: () => "rust-lld", linkArgs: ["--build-id=none"] },
19
+ aarch64: { triple: "aarch64-unknown-linux-musl", linker: () => "rust-lld", linkArgs: ["--build-id=none"] },
20
+ armv7: {
21
+ triple: "armv7-unknown-linux-gnueabihf",
22
+ linker: (toolkit) => join(toolkit, ARMV7_GCC),
23
+ linkArgs: ["-Wl,--build-id=none", "-Wl,--discard-all"],
24
+ toolkit: {
25
+ url: "https://global.synologydownload.com/download/ToolChain/toolkit/7.0/alpine4k/ds.alpine4k-7.0.env.txz",
26
+ sha256: "767cb27541e3c2aae5a07bc1078809956992c75a54e6bfca2275fa633e983407",
27
+ },
28
+ interpreter: ARMV7_INTERPRETER,
29
+ },
30
+ };
31
+ /** Go's `-X` values for a Rust App, read with `option_env!`; every one is set, empty when unused. */
32
+ export function rustBuildEnv(manifest) {
33
+ const { capabilities, executor } = manifest;
34
+ const delegate = capabilities.delegate;
35
+ return {
36
+ SYNOPKGLAND_PACKAGE: manifest.package,
37
+ SYNOPKGLAND_VERSION: manifest.spkVersion,
38
+ SYNOPKGLAND_PERMISSIONS_DIGEST: manifest.apiPermissionsDigest ?? "",
39
+ SYNOPKGLAND_SDK_PERMISSIONS_DIGEST: manifest.apiSDKPermissionsDigest ?? "",
40
+ SYNOPKGLAND_EXECUTOR_DIGEST: executor?.executorDigest ?? "",
41
+ SYNOPKGLAND_POLICY_DIGEST: executor?.policyDigest ?? "",
42
+ SYNOPKGLAND_DELEGATE_SCOPE: delegate?.scope ?? "",
43
+ SYNOPKGLAND_DELEGATE_OPERATIONS: delegate ? JSON.stringify([...delegate.operations].sort()) : "",
44
+ SYNOPKGLAND_DELEGATE_DESTINATIONS: delegate ? JSON.stringify(delegate.destinations ? [...delegate.destinations].sort() : []) : "",
45
+ SYNOPKGLAND_DELEGATE_LIFETIME: delegate?.lifetime ?? "",
46
+ SYNOPKGLAND_IDENTITY: capabilities.identity ?? "",
47
+ SYNOPKGLAND_FILE_STATION: capabilities.files || delegate ? "session" : "",
48
+ SYNOPKGLAND_ENTRY: manifest.entry ?? "",
49
+ SYNOPKGLAND_PORTAL: manifest.portal ? (portalByExecutor(manifest.os_min_ver) ? "executor" : "on") : "",
50
+ };
51
+ }
52
+ /** Builds the CGI bin, and the worker bin with capabilities.jobs, in one `cargo build` into `work`. */
53
+ export async function buildRustBackend(manifest, arch, work, run, { host = { platform: process.platform, arch: process.arch }, prepareToolkit: prepare = prepareToolkit } = {}) {
54
+ const backend = manifest.backend;
55
+ if (backend.kind !== "rust")
56
+ throw new Error("buildRustBackend needs backend.kind: rust");
57
+ const target = RUST_TARGETS[arch];
58
+ if (target.toolkit && (host.platform !== "linux" || host.arch !== "x64")) {
59
+ fail(`${arch} links with the gcc of the DSM toolkit, which runs on Linux x86_64 only; build ${arch} on Linux x86_64 (for example in a container).`);
60
+ }
61
+ const bins = [backend.bin, ...(manifest.capabilities.jobs ? [backend.worker] : [])];
62
+ const toolkit = target.toolkit ? await prepare(backend.dir, arch, [target.toolkit], run).catch((error) => fail(`DSM toolkit for ${arch}: ${error.message}`)) : "";
63
+ try {
64
+ const result = await run("cargo", [
65
+ "build", "--release", "--locked", "--target", target.triple, "-p", backend.package,
66
+ ...bins.flatMap(bin => ["--bin", bin]), "--message-format=json-render-diagnostics",
67
+ ], { cwd: backend.dir, env: cargoEnv(manifest, backend.dir, target, toolkit), capture: true });
68
+ if (result.missing)
69
+ fail("找不到 cargo。請以 rustup 安裝 Rust(https://rustup.rs);backend/rust-toolchain.toml 會選定版本與 target。");
70
+ if (result.status !== 0)
71
+ fail(`cargo build(${arch})失敗。\n${result.stderr}`);
72
+ const [cgi, worker] = bins.map((bin, i) => {
73
+ const out = join(work, i === 0 ? `api-${arch}.cgi` : `background-worker-${arch}`);
74
+ copyFileSync(artifact(result.stdout ?? "", bin), out);
75
+ const problem = checkCgiBinary(work, out, arch, target.interpreter);
76
+ if (problem)
77
+ throw new StandaloneBuildError(problem.code, problem.message);
78
+ return out;
79
+ });
80
+ return { cgi: cgi, ...(worker ? { worker } : {}) };
81
+ }
82
+ finally {
83
+ if (toolkit)
84
+ rmSync(toolkit, { recursive: true, force: true });
85
+ }
86
+ }
87
+ /**
88
+ * The environment of one reproducible cargo build: the CLI's flags replace any
89
+ * inherited RUSTFLAGS, paths are remapped, symbols and build IDs dropped. Cargo
90
+ * fetches the private SDK repository through git, with the user's credentials.
91
+ */
92
+ function cargoEnv(manifest, dir, target, toolkit) {
93
+ const env = { ...process.env, ...rustBuildEnv(manifest) };
94
+ delete env.RUSTFLAGS;
95
+ const cargoHome = env.CARGO_HOME || join(homedir(), ".cargo");
96
+ env.CARGO_ENCODED_RUSTFLAGS = [
97
+ `--remap-path-prefix=${dir}=/build`,
98
+ `--remap-path-prefix=${cargoHome}=/cargo`,
99
+ "-Cstrip=symbols",
100
+ ...target.linkArgs.map(arg => `-Clink-arg=${arg}`),
101
+ ].join("\x1f");
102
+ env[`CARGO_TARGET_${target.triple.toUpperCase().replaceAll("-", "_")}_LINKER`] = target.linker(toolkit);
103
+ env.SOURCE_DATE_EPOCH = "0";
104
+ env.CARGO_NET_GIT_FETCH_WITH_CLI = "true";
105
+ if (env.SYNOPKGLAND_SDK_DIR) {
106
+ // Development only: git reads the SDK repository from this checkout, which must hold the locked tag.
107
+ const n = Number(env.GIT_CONFIG_COUNT ?? 0);
108
+ env.GIT_CONFIG_COUNT = String(n + 1);
109
+ env[`GIT_CONFIG_KEY_${n}`] = `url.file://${env.SYNOPKGLAND_SDK_DIR}.insteadOf`;
110
+ env[`GIT_CONFIG_VALUE_${n}`] = SDK_GIT_URL;
111
+ }
112
+ return env;
113
+ }
114
+ /** The executable Cargo reported for `bin`. */
115
+ function artifact(messages, bin) {
116
+ for (const line of messages.split("\n")) {
117
+ if (!line.startsWith("{"))
118
+ continue;
119
+ const message = JSON.parse(line);
120
+ if (message.reason === "compiler-artifact" && message.target?.kind?.includes("bin") && message.target.name === bin && message.executable)
121
+ return message.executable;
122
+ }
123
+ return fail(`cargo build 沒有產出 bin ${bin};確認 backend/src/bin/${bin}.rs 存在。`);
124
+ }
125
+ function fail(message) {
126
+ throw new StandaloneBuildError("STANDALONE_RUST_BUILD_FAILED", message);
127
+ }
@@ -1,9 +1,11 @@
1
+ import { type SdkBackend } from "../runtime/sdk-source.js";
1
2
  export interface SdkProblem {
2
3
  code: string;
3
4
  message: string;
4
5
  }
5
6
  /**
6
- * backend/go.mod names the App's SDK version; the frontend package and this
7
- * CLI must agree with it. `needsSdk`: the App declares delegate or rootOps.
7
+ * The backend's dependency files name the App's SDK version; the frontend
8
+ * package and this CLI must agree with it. `needsSdk`: the App declares
9
+ * delegate or rootOps.
8
10
  */
9
- export declare function sdkProblems(backendDir: string, frontendDir: string | undefined, needsSdk: boolean, cliVersion?: string): SdkProblem[];
11
+ export declare function sdkProblems(backend: SdkBackend, frontendDir: string | undefined, needsSdk: boolean, cliVersion?: string): SdkProblem[];
@@ -1,24 +1,37 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { CLI_VERSION } from "../cli-version.js";
4
- import { SDK_MODULE, SDK_PACKAGE, sdkRequirement } from "../runtime/sdk-source.js";
4
+ import { appSdkVersion, SDK_DEPENDENCY, SDK_PACKAGE } from "../runtime/sdk-source.js";
5
5
  const minorLine = (version) => version.split(".").slice(0, 2).join(".");
6
+ /** The file that names each backend kind's SDK version. */
7
+ const SDK_VERSION_FILE = { go: "backend/go.mod", rust: "backend/Cargo.lock" };
6
8
  /**
7
- * backend/go.mod names the App's SDK version; the frontend package and this
8
- * CLI must agree with it. `needsSdk`: the App declares delegate or rootOps.
9
+ * The backend's dependency files name the App's SDK version; the frontend
10
+ * package and this CLI must agree with it. `needsSdk`: the App declares
11
+ * delegate or rootOps.
9
12
  */
10
- export function sdkProblems(backendDir, frontendDir, needsSdk, cliVersion = CLI_VERSION) {
11
- const version = sdkRequirement(backendDir);
13
+ export function sdkProblems(backend, frontendDir, needsSdk, cliVersion = CLI_VERSION) {
14
+ let version;
15
+ try {
16
+ version = appSdkVersion(backend);
17
+ }
18
+ catch (error) {
19
+ const tag = /^\[SDK_TAG_REQUIRED\] ([\s\S]*)$/.exec(error.message);
20
+ if (tag)
21
+ return [{ code: "SDK_TAG_REQUIRED", message: tag[1] }];
22
+ throw error;
23
+ }
12
24
  if (!version) {
13
- return needsSdk ? [{ code: "SDK_REQUIRED", message: `capabilities.delegate 與 rootOps 需要 SDK:在 backend/go.mod 加入 require ${SDK_MODULE} v${minorLine(cliVersion)}.x。` }] : [];
25
+ return needsSdk ? [{ code: "SDK_REQUIRED", message: `capabilities.delegate 與 rootOps 需要 SDK(${minorLine(cliVersion)}.x):${SDK_DEPENDENCY[backend.kind]}` }] : [];
14
26
  }
27
+ const file = SDK_VERSION_FILE[backend.kind];
15
28
  const problems = [];
16
29
  if (minorLine(version) !== minorLine(cliVersion)) {
17
- problems.push({ code: "SDK_UNSUPPORTED", message: `backend/go.mod 要求 SDK ${version};CLI ${cliVersion} 只支援 SDK ${minorLine(cliVersion)}.x。請改用對應的 CLI,或把 SDK 升到 ${minorLine(cliVersion)}.x。` });
30
+ problems.push({ code: "SDK_UNSUPPORTED", message: `${file} 要求 SDK ${version};CLI ${cliVersion} 只支援 SDK ${minorLine(cliVersion)}.x。請改用對應的 CLI,或把 SDK 升到 ${minorLine(cliVersion)}.x。` });
18
31
  }
19
32
  const frontend = frontendDir === undefined ? undefined : frontendSdkVersion(frontendDir);
20
33
  if (frontend && frontend.version !== version) {
21
- problems.push({ code: "SDK_VERSION_MISMATCH", message: `frontend/package.json 的 ${SDK_PACKAGE} 是 ${frontend.spec}(${frontend.version ?? "讀不到版本"}),backend/go.mod 要求 ${version};兩者必須相同。` });
34
+ problems.push({ code: "SDK_VERSION_MISMATCH", message: `frontend/package.json 的 ${SDK_PACKAGE} 是 ${frontend.spec}(${frontend.version ?? "讀不到版本"}),${file} 要求 ${version};兩者必須相同。` });
22
35
  }
23
36
  return problems;
24
37
  }
@@ -22,10 +22,12 @@ import { copyDaemonPayload } from "./daemon.js";
22
22
  import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit } from "./background.js";
23
23
  import { renderStandaloneResource } from "./resource.js";
24
24
  import { nativePortal } from "./portal.js";
25
+ import { portalByExecutor } from "../build-spk/dsm-version.js";
25
26
  import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalMustache, renderPortalWizardStep } from "../portal/package.js";
26
27
  import { executorPlan } from "../runtime/executor-build.js";
27
28
  import { EXECUTOR_ANCHOR } from "../runtime/executor-inspect.js";
28
29
  import { renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps } from "./shared-folder.js";
30
+ import { stateItemTexts, uninstallExecProblem, uninstallRecord } from "./uninstall.js";
29
31
  const FILE_MODE = 0o644;
30
32
  const EXEC_MODE = 0o755;
31
33
  const MAX_UI_BYTES = 64 * 1024 * 1024;
@@ -83,7 +85,10 @@ export async function assembleStandaloneSpk(options) {
83
85
  copyFileSync(options.backgroundWorker, worker);
84
86
  chmodSync(worker, EXEC_MODE);
85
87
  }
86
- copyStandaloneNotices(manifest.root, manifest.ui.dir, join(staging, 'package'), manifest.backend.kind === 'go' || Boolean(manifest.daemon), manifest.backend.kind === 'go' ? manifest.backend.dir : undefined);
88
+ const backend = manifest.backend;
89
+ // A daemon is a Go program of the project's backend directory.
90
+ const sdk = backend.kind !== 'binary' ? backend : manifest.daemon ? { kind: 'go', dir: join(manifest.root, 'backend') } : undefined;
91
+ copyStandaloneNotices(manifest.root, manifest.ui.dir, join(staging, 'package'), { go: backend.kind === 'go' || Boolean(manifest.daemon), rust: backend.kind === 'rust', sdk });
87
92
  if (manifest.daemon)
88
93
  copyFileSync(new URL('../../assets/licenses/TEMPLATE.txt', import.meta.url), join(staging, 'package', 'LICENSES', 'DAEMON-LICENSE.txt'));
89
94
  const portal = nativePortal(manifest);
@@ -103,11 +108,14 @@ export async function assembleStandaloneSpk(options) {
103
108
  for (const op of manifest.rootOps ?? [])
104
109
  writeFile(join(staging, "package", "root", op.name + ".sh"), op.body);
105
110
  }
111
+ const hook = manifest.uninstall && uninstallExecProblem(join(staging, "package"), manifest.uninstall.purge);
112
+ if (hook)
113
+ throw new StandaloneBuildError(hook.code, hook.message);
106
114
  const packageTgz = join(staging, "package.tgz");
107
115
  await createReproducibleTar(join(staging, "package"), packageTgz, { gzip: true });
108
116
  rmSync(join(staging, "package"), { recursive: true, force: true });
109
117
  const checksum = createHash("md5").update(readFileSync(packageTgz)).digest("hex");
110
- writeFile(join(staging, "sdk-package.json"), JSON.stringify({ schema: 2, backend: manifest.backend.kind, capabilities: Object.keys(manifest.capabilities).sort(), ...(manifest.entry ? { entry: manifest.entry } : {}) }));
118
+ writeFile(join(staging, "sdk-package.json"), JSON.stringify({ schema: 2, backend: manifest.backend.kind, capabilities: Object.keys(manifest.capabilities).sort(), ...(manifest.entry ? { entry: manifest.entry } : {}), ...(manifest.uninstall ? { uninstall: uninstallRecord(manifest.uninstall, manifest.i18n?.catalogs) } : {}) }));
111
119
  writeFile(join(staging, "INFO"), renderInfo(manifest, options.arch, checksum, options.executor?.executorDigest));
112
120
  mkdirSync(join(staging, "conf"));
113
121
  writeFile(join(staging, "conf", "privilege"), renderNativePrivilege());
@@ -131,7 +139,11 @@ export async function assembleStandaloneSpk(options) {
131
139
  ...install.static,
132
140
  ...install.dynamic,
133
141
  ...(folder ? renderSharedFolderUpgradeWizardFiles(manifest.package, folder) : {}),
134
- ...renderUninstallWizardFiles(manifest.package, "native", { executor: Boolean(options.executor), portal: Boolean(portal) }),
142
+ ...renderUninstallWizardFiles(manifest.package, "native", {
143
+ executor: Boolean(options.executor), portal: Boolean(portal), portalEntry: Boolean(manifest.portal && portalByExecutor(manifest.os_min_ver)),
144
+ rootPurge: Boolean(manifest.uninstall?.purge.root),
145
+ ...(manifest.uninstall?.state ? { state: { items: (locale) => stateItemTexts(manifest.uninstall, manifest.i18n?.catalogs, locale) } } : {}),
146
+ }),
135
147
  });
136
148
  writePackageCenterIcons(staging, icons);
137
149
  mkdirSync(dirname(options.outputPath), { recursive: true });
@@ -0,0 +1,10 @@
1
+ import type { CommandRunner } from './build.js';
2
+ export type Toolkit = {
3
+ url: string;
4
+ sha256: string;
5
+ };
6
+ export declare function verifyToolkitArchive(path: string, expected: string): void;
7
+ /** One cache per user, shared by every project and worktree; archives are checked against sha256 on every use. */
8
+ export declare function toolkitCacheDir(env: Record<string, string | undefined>, platform: string, home: string): string;
9
+ /** Extracts the checksum-pinned toolkit archives of one architecture into a fresh folder the caller removes; '' without archives. */
10
+ export declare function prepareToolkit(cwd: string, arch: string, inputs: Toolkit[], run: CommandRunner): Promise<string>;
@@ -0,0 +1,101 @@
1
+ /** Checksum-pinned toolkit archives: one private download cache per user, re-extracted from verified bytes on every build. */
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { isAbsolute, join, resolve } from 'node:path';
6
+ function fail(message) { throw Object.assign(new Error(`NATIVE_LAYOUT_INVALID: ${message}`), { code: 'NATIVE_LAYOUT_INVALID' }); }
7
+ const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
8
+ export function verifyToolkitArchive(path, expected) {
9
+ if (!lstatSync(path).isFile() || digest(readFileSync(path)) !== expected)
10
+ fail('toolkit checksum mismatch');
11
+ }
12
+ /** One cache per user, shared by every project and worktree; archives are checked against sha256 on every use. */
13
+ export function toolkitCacheDir(env, platform, home) {
14
+ if (env.SYNOPKGLAND_CACHE_DIR)
15
+ return resolve(env.SYNOPKGLAND_CACHE_DIR);
16
+ if (platform === 'darwin')
17
+ return join(home, 'Library', 'Caches', 'synopkgland');
18
+ const xdg = env.XDG_CACHE_HOME;
19
+ return join(xdg && isAbsolute(xdg) ? xdg : join(home, '.cache'), 'synopkgland');
20
+ }
21
+ // Concurrent architectures that pin the same archive share one download.
22
+ const downloads = new Map();
23
+ async function download(cwd, cache, input, archive, run) {
24
+ const tmp = mkdtempSync(join(cache, '.download-'));
25
+ try {
26
+ const out = join(tmp, 'archive');
27
+ const r = await run('curl', ['--fail', '--location', '--proto', '=https', '--proto-redir', '=https', '--retry', '2', '--retry-all-errors', '--connect-timeout', '20', '--max-time', '600', '--output', out, input.url], { cwd, env: { ...process.env } });
28
+ if (r.status !== 0)
29
+ fail('toolkit download failed');
30
+ // Rename is atomic, so another build process sees either no archive or a verified one.
31
+ verifyToolkitArchive(out, input.sha256);
32
+ renameSync(out, archive);
33
+ }
34
+ finally {
35
+ rmSync(tmp, { recursive: true, force: true });
36
+ }
37
+ }
38
+ /** Extracts the checksum-pinned toolkit archives of one architecture into a fresh folder the caller removes; '' without archives. */
39
+ export async function prepareToolkit(cwd, arch, inputs, run) {
40
+ if (!inputs.length)
41
+ return '';
42
+ const cache = toolkitCacheDir(process.env, process.platform, homedir());
43
+ mkdirSync(cache, { recursive: true, mode: 0o700 });
44
+ if (lstatSync(cache).isSymbolicLink() || (lstatSync(cache).mode & 0o022))
45
+ fail('toolkit cache must be owned and private');
46
+ const archives = [];
47
+ for (const input of inputs) {
48
+ const archive = join(cache, `${input.sha256}.txz`);
49
+ if (!existsSync(archive)) {
50
+ let pending = downloads.get(archive);
51
+ if (!pending) {
52
+ pending = download(cwd, cache, input, archive, run).finally(() => downloads.delete(archive));
53
+ downloads.set(archive, pending);
54
+ }
55
+ await pending;
56
+ }
57
+ verifyToolkitArchive(archive, input.sha256);
58
+ archives.push(archive);
59
+ }
60
+ // Re-extract verified bytes: a package-writable marker never authenticates an extracted tree.
61
+ const stage = mkdtempSync(join(cache, `toolkit-${arch}-`));
62
+ try {
63
+ const flavor = await tarFlavor(cwd, run);
64
+ for (const archive of archives) {
65
+ if (flavor === 'bsdtar')
66
+ await refuseEscapingMembers(cwd, archive, run);
67
+ const r = await run('tar', flavor === 'gnu' ? ['-xJf', archive, '-C', stage, '--keep-directory-symlink'] : ['-xPJf', archive, '-C', stage], { cwd, env: { ...process.env } });
68
+ if (r.status !== 0)
69
+ fail('toolkit extraction failed');
70
+ }
71
+ return stage;
72
+ }
73
+ catch (e) {
74
+ rmSync(stage, { recursive: true, force: true });
75
+ throw e;
76
+ }
77
+ }
78
+ /**
79
+ * A later toolkit archive may write through a directory symlink an earlier one
80
+ * created. GNU tar keeps such a symlink only with --keep-directory-symlink;
81
+ * bsdtar only with -P, which also allows absolute and .. members, so those are
82
+ * refused from the listing first.
83
+ */
84
+ async function tarFlavor(cwd, run) {
85
+ const r = await run('tar', ['--version'], { cwd, env: { ...process.env }, capture: true });
86
+ const version = r.status === 0 ? r.stdout ?? '' : '';
87
+ if (/\bGNU tar\b/.test(version))
88
+ return 'gnu';
89
+ if (/\bbsdtar\b/.test(version))
90
+ return 'bsdtar';
91
+ throw Object.assign(new Error(`NATIVE_TOOLKIT_TAR_UNSUPPORTED: extracting native.toolkits needs GNU tar or bsdtar as \`tar\`; found ${r.missing ? 'no tar' : JSON.stringify(version.split('\n')[0] ?? '')}`), { code: 'NATIVE_TOOLKIT_TAR_UNSUPPORTED' });
92
+ }
93
+ async function refuseEscapingMembers(cwd, archive, run) {
94
+ const r = await run('tar', ['-tJf', archive], { cwd, env: { ...process.env }, capture: true });
95
+ if (r.status !== 0)
96
+ fail('toolkit extraction failed');
97
+ for (const member of (r.stdout ?? '').split('\n').filter(Boolean)) {
98
+ if (member.startsWith('/') || member.split('/').includes('..'))
99
+ fail(`toolkit member escapes the toolkit folder: ${member}`);
100
+ }
101
+ }
@@ -0,0 +1,10 @@
1
+ import type { StandaloneManifest } from "./manifest.js";
2
+ /** The syslog tag of a failed or timed-out App uninstall hook. */
3
+ export declare const UNINSTALL_SYSLOG_TAG = "synopkgland-uninstall";
4
+ /**
5
+ * `synopkgland_uninstall_hook`: runs `uninstall.purge` as the package account
6
+ * with a fixed environment and the wizard's two answers, within its timeout.
7
+ * A failure is logged and never blocks the uninstall. It sets
8
+ * `synopkgland_purge_args` for the executor purge request.
9
+ */
10
+ export declare function renderUninstallHook(manifest: StandaloneManifest): string;