@synopackageland/cli 0.5.0 → 0.5.3

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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist/authorization-sdk.d.ts +5 -0
  3. package/dist/authorization-sdk.js +5 -0
  4. package/dist/build-spk/delete-app-data.d.ts +1 -1
  5. package/dist/build-spk/uninstall-wizard.d.ts +1 -1
  6. package/dist/build-spk/uninstall-wizard.js +1 -1
  7. package/dist/cli.js +2 -2
  8. package/dist/error-registry.js +3 -3
  9. package/dist/host/dsm-port-profile.d.ts +1 -1
  10. package/dist/host/dsm-port-profile.js +1 -1
  11. package/dist/init.js +25 -4
  12. package/dist/standalone/authorization-window.d.ts +2 -0
  13. package/dist/standalone/authorization-window.js +28 -0
  14. package/dist/standalone/authorization.d.ts +48 -0
  15. package/dist/standalone/authorization.js +116 -0
  16. package/dist/standalone/build.js +18 -6
  17. package/dist/standalone/dsm-window.js +8 -1
  18. package/dist/standalone/helper.js +1 -1
  19. package/dist/standalone/inspect-helper.js +12 -2
  20. package/dist/standalone/inspect-spk.js +11 -0
  21. package/dist/standalone/lifecycle-helper.d.ts +1 -1
  22. package/dist/standalone/lifecycle-helper.js +2 -2
  23. package/dist/standalone/lifecycle.d.ts +1 -1
  24. package/dist/standalone/lifecycle.js +3 -3
  25. package/dist/standalone/root-ops.d.ts +2 -0
  26. package/dist/standalone/root-ops.js +10 -0
  27. package/dist/standalone/root-window.js +1 -1
  28. package/dist/standalone/spk.d.ts +2 -0
  29. package/dist/standalone/spk.js +14 -0
  30. package/dist/templates/_authorization/main.go +104 -0
  31. package/dist/templates/_authorization/main_test.go +35 -0
  32. package/dist/templates/native-go/README.md +25 -1
  33. package/dist/templates/native-go/backend/cmd/app/main.go +8 -6
  34. package/dist/templates/native-go/backend/cmd/app/sdk_main.go +8 -6
  35. package/dist/templates/native-go/backend/cmd/helper/main.go +18 -1
  36. package/dist/templates/native-go/backend/internal/app/authorization_none.go +5 -0
  37. package/dist/templates/native-go/backend/internal/app/routes.go +5 -2
  38. package/dist/templates/native-go/backend/internal/app/sdk_routes.go +5 -2
  39. package/dist/templates/native-go/backend/internal/helper/management.go +8 -1
  40. package/dist/templates/native-go/backend/internal/helper/purge.go +115 -0
  41. package/dist/templates/native-go/backend/internal/helper/purge_test.go +59 -0
  42. package/dist/templates/native-go/backend/internal/helper/revoke.go +109 -0
  43. package/dist/templates/native-go/backend/internal/helper/revoke_test.go +121 -0
  44. package/dist/templates/native-go/backend/internal/helper/sdk_server_linux.go +11 -2
  45. package/dist/templates/native-go/backend/internal/helper/server_linux.go +11 -2
  46. package/dist/templates/native-go/dsm-package.yaml +1 -1
  47. package/dist/templates/native-go-authorization/backend/internal/app/authorization.go +298 -0
  48. package/dist/templates/native-go-authorization/backend/internal/app/authorization_test.go +96 -0
  49. package/dist/templates/native-go-authorization/frontend/src/authorization/AuthorizationGate.vue +89 -0
  50. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/de.json +36 -0
  51. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/en.json +36 -0
  52. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/es.json +36 -0
  53. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/fr.json +36 -0
  54. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/index.ts +15 -0
  55. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/it.json +36 -0
  56. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/ja.json +36 -0
  57. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/ko.json +36 -0
  58. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/zh-CN.json +36 -0
  59. package/dist/templates/native-go-authorization/frontend/src/authorization/i18n/zh-TW.json +36 -0
  60. package/dist/templates/native-go-authorization/frontend/test/authorization-preview.html +14 -0
  61. package/dist/templates/native-go-authorization/frontend/test/authorization.test.ts +59 -0
  62. package/dist/templates/native-go-vue/frontend/src/context.ts +3 -0
  63. package/dist/types.d.ts +1 -1
  64. package/package.json +5 -2
  65. package/skills/reference/create.md +2 -2
  66. package/templates/_authorization/main.go +104 -0
  67. package/templates/_authorization/main_test.go +35 -0
  68. package/templates/native-go/README.md +25 -1
  69. package/templates/native-go/backend/cmd/app/main.go +8 -6
  70. package/templates/native-go/backend/cmd/app/sdk_main.go +8 -6
  71. package/templates/native-go/backend/cmd/helper/main.go +18 -1
  72. package/templates/native-go/backend/internal/app/authorization_none.go +5 -0
  73. package/templates/native-go/backend/internal/app/routes.go +5 -2
  74. package/templates/native-go/backend/internal/app/sdk_routes.go +5 -2
  75. package/templates/native-go/backend/internal/helper/management.go +8 -1
  76. package/templates/native-go/backend/internal/helper/purge.go +115 -0
  77. package/templates/native-go/backend/internal/helper/purge_test.go +59 -0
  78. package/templates/native-go/backend/internal/helper/revoke.go +109 -0
  79. package/templates/native-go/backend/internal/helper/revoke_test.go +121 -0
  80. package/templates/native-go/backend/internal/helper/sdk_server_linux.go +11 -2
  81. package/templates/native-go/backend/internal/helper/server_linux.go +11 -2
  82. package/templates/native-go/dsm-package.yaml +1 -1
  83. package/templates/native-go-authorization/backend/internal/app/authorization.go +298 -0
  84. package/templates/native-go-authorization/backend/internal/app/authorization_test.go +96 -0
  85. package/templates/native-go-authorization/frontend/src/authorization/AuthorizationGate.vue +89 -0
  86. package/templates/native-go-authorization/frontend/src/authorization/i18n/de.json +36 -0
  87. package/templates/native-go-authorization/frontend/src/authorization/i18n/en.json +36 -0
  88. package/templates/native-go-authorization/frontend/src/authorization/i18n/es.json +36 -0
  89. package/templates/native-go-authorization/frontend/src/authorization/i18n/fr.json +36 -0
  90. package/templates/native-go-authorization/frontend/src/authorization/i18n/index.ts +15 -0
  91. package/templates/native-go-authorization/frontend/src/authorization/i18n/it.json +36 -0
  92. package/templates/native-go-authorization/frontend/src/authorization/i18n/ja.json +36 -0
  93. package/templates/native-go-authorization/frontend/src/authorization/i18n/ko.json +36 -0
  94. package/templates/native-go-authorization/frontend/src/authorization/i18n/zh-CN.json +36 -0
  95. package/templates/native-go-authorization/frontend/src/authorization/i18n/zh-TW.json +36 -0
  96. package/templates/native-go-authorization/frontend/test/authorization-preview.html +14 -0
  97. package/templates/native-go-authorization/frontend/test/authorization.test.ts +59 -0
  98. package/templates/native-go-vue/frontend/src/context.ts +3 -0
  99. package/dist/templates/hello-web/compose.yaml +0 -24
  100. package/dist/templates/hello-web/html/app.js +0 -1
  101. package/dist/templates/hello-web/html/index.html +0 -13
  102. package/dist/templates/hello-web/synology-app.yaml +0 -16
  103. package/dist/templates/hello-web-page/compose.yaml +0 -24
  104. package/dist/templates/hello-web-page/html/app.js +0 -1
  105. package/dist/templates/hello-web-page/html/index.html +0 -13
  106. package/dist/templates/hello-web-page/synology-app.yaml +0 -16
  107. package/templates/hello-web/compose.yaml +0 -24
  108. package/templates/hello-web/html/app.js +0 -1
  109. package/templates/hello-web/html/index.html +0 -13
  110. package/templates/hello-web/synology-app.yaml +0 -16
  111. package/templates/hello-web-page/compose.yaml +0 -24
  112. package/templates/hello-web-page/html/app.js +0 -1
  113. package/templates/hello-web-page/html/index.html +0 -13
  114. package/templates/hello-web-page/synology-app.yaml +0 -16
package/README.md CHANGED
@@ -15,7 +15,7 @@ synopkgland --project ./my-app deploy --dry-run --arch x86_64
15
15
  synopkgland --project ./my-app deploy --host my-nas --arch x86_64
16
16
  ```
17
17
 
18
- Native-go requires Go 1.24+; binary consumes checked prebuilt archives; container SPKs use DSM resource workers. Root operations require DSM 7.1-42661 or later and per-operation DSM approval. DSM 7.1 keeps the native password dialog but creates root tasks using the admin session without a server-side password token; DSM 7.2+ retains its one-use token. See the native-go template README for the API matrix and completion limits. Remove root tasks before uninstall; changed task text needs fresh approval.
18
+ Native-go requires Go 1.25+; binary consumes checked prebuilt archives; container SPKs use DSM resource workers. Root operations require DSM 7.1-42661 or later and per-operation DSM approval. DSM 7.1 keeps the native password dialog but creates root tasks using the admin session without a server-side password token; DSM 7.2+ retains its one-use token. See the native-go template README for the API matrix and completion limits. Remove root tasks before uninstall; changed task text needs fresh approval.
19
19
 
20
20
  `--ui vue --integration go|better-auth` generates a package-account integration
21
21
  (`privilege: none`, no helper). The privileged helper needs an explicit
@@ -0,0 +1,5 @@
1
+ /** Public build-time adapter surface for native-layout products. */
2
+ export { ROOT_RUNTIME, type RootTask } from './standalone/root-window.js';
3
+ export { AUTHORIZATION_RUNTIME } from './standalone/authorization-window.js';
4
+ /** Canonical Vue gate and nine catalogs, shipped unchanged in the npm archive. */
5
+ export declare const authorizationTemplateDirectory: URL;
@@ -0,0 +1,5 @@
1
+ /** Public build-time adapter surface for native-layout products. */
2
+ export { ROOT_RUNTIME } from './standalone/root-window.js';
3
+ export { AUTHORIZATION_RUNTIME } from './standalone/authorization-window.js';
4
+ /** Canonical Vue gate and nine catalogs, shipped unchanged in the npm archive. */
5
+ export const authorizationTemplateDirectory = new URL('../templates/native-go-authorization/frontend/src/authorization/', import.meta.url);
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * DSM keeps `@appdata`, `@apphome` and `@appconf` after an uninstall, so a
5
5
  * package that offers deletion has to do it itself. The uninstall wizard's
6
- * `pkgwizard_remove_additional` box (the key Synology's own packages use) is
6
+ * `pkgwizard_remove_additional` box (the key DSM's built-in packages use) is
7
7
  * unchecked by default, so an absent variable leaves the data in place. The
8
8
  * pre-0.5 key `delete_app_data` and DSM's `wizard_` mirror of either key are
9
9
  * still honoured for scripted callers.
@@ -1,4 +1,4 @@
1
- /** The key Synology's own packages use for their "also remove my data" box. */
1
+ /** The key DSM's built-in packages use for their "also remove my data" box. */
2
2
  export declare const REMOVE_ADDITIONAL_KEY = "pkgwizard_remove_additional";
3
3
  export type UninstallWizardKind = "native" | "container";
4
4
  /** Options that add a warning without changing the data box. */
@@ -1,5 +1,5 @@
1
1
  import { renderWizardFiles } from "./wizard-i18n.js";
2
- /** The key Synology's own packages use for their "also remove my data" box. */
2
+ /** The key DSM's built-in packages use for their "also remove my data" box. */
3
3
  export const REMOVE_ADDITIONAL_KEY = "pkgwizard_remove_additional";
4
4
  function list(items) {
5
5
  return `<ul class="synopkg-ul-list">${items.map((item) => `<li>${item}</li>`).join("")}</ul>`;
package/dist/cli.js CHANGED
@@ -247,14 +247,14 @@ export async function runCli(argv) {
247
247
  .addHelpText("after", `
248
248
  模板分兩類:
249
249
  獨立 Container / Binary App(synology-app.yaml):
250
- 容器(compose.yaml):hello-web、hello-web-page、hello-files、compose-import
250
+ 容器(compose.yaml):hello-files、compose-import
251
251
  binary(native.yaml):執行一個預先建置的 binary 服務;可用 adminport 在自己的 port 提供網頁
252
252
  Standalone Package(未簽署原生 SPK,含 Go CGI 與 DSM 視窗,dsm-package.yaml):
253
253
  native-go:Go CGI backend + DSM 視窗 UI,預設需登入、admin 動作另外宣告。
254
254
  --privilege root:另加固定的 root 操作(root/<名稱>.sh),由 DSM 管理員在套件視窗
255
255
  以密碼核准後才能執行;預設 none,完全不用 root。
256
256
 
257
- 快速開始(native-go,需要 Go 1.24+):
257
+ 快速開始(native-go,需要 Go 1.25+):
258
258
  synopkgland init -t native-go my-app
259
259
  cd my-app && $EDITOR backend/internal/app/routes.go frontend/src/App.vue
260
260
  synopkgland build-spk # → dist/<Package>-<version>-<arch>.spk
@@ -651,7 +651,7 @@ export const ERROR_REGISTRY = {
651
651
  "STANDALONE_BACKGROUND_BUILD_FAILED": {
652
652
  "code": "STANDALONE_BACKGROUND_BUILD_FAILED",
653
653
  "description": "無法建置套件的背景 worker。",
654
- "suggestion": "安裝 Go 1.24 以上,確認 backend 含 cmd/background-worker(可用 synopkgland init --background 建立),並檢查 compiler stderr。"
654
+ "suggestion": "安裝 Go 1.25 以上,確認 backend 含 cmd/background-worker(可用 synopkgland init --background 建立),並檢查 compiler stderr。"
655
655
  },
656
656
  "STANDALONE_BACKGROUND_ARTIFACT_REQUIRED": {
657
657
  "code": "STANDALONE_BACKGROUND_ARTIFACT_REQUIRED",
@@ -836,7 +836,7 @@ export const ERROR_REGISTRY = {
836
836
  "STANDALONE_GO_MISSING": {
837
837
  "code": "STANDALONE_GO_MISSING",
838
838
  "description": "找不到 go 指令。",
839
- "suggestion": "請安裝 Go 1.24 以上版本(https://go.dev/dl/),或改用 backend.binary 打包既有執行檔。"
839
+ "suggestion": "請安裝 Go 1.25 以上版本(https://go.dev/dl/),或改用 backend.binary 打包既有執行檔。"
840
840
  },
841
841
  "STANDALONE_GO_BUILD_FAILED": {
842
842
  "code": "STANDALONE_GO_BUILD_FAILED",
@@ -880,7 +880,7 @@ export const ERROR_REGISTRY = {
880
880
  },
881
881
  "STANDALONE_I18N_GLOSSARY_MISMATCH": {
882
882
  "code": "STANDALONE_I18N_GLOSSARY_MISMATCH",
883
- "description": "訊息沒有使用用語表中的 Synology 官方用語。",
883
+ "description": "訊息沒有使用用語表中的 DSM 介面用語。",
884
884
  "suggestion": "請改用 glossary.json 中該語言的用語,與 DSM 介面保持一致。"
885
885
  },
886
886
  "DEPLOY_VERSION_INVALID": {
@@ -36,7 +36,7 @@ export declare function installedPackageIdentitiesFromPayload(payload: unknown):
36
36
  *
37
37
  * The package list is a forward mapping: package identity -> project name.
38
38
  * Do not use presumedPackageIdentity here; replacing dashes with dots is
39
- * lossy for identities such as com.example.hello-web.
39
+ * lossy for identities such as com.example.container-web.
40
40
  */
41
41
  export declare function dsmPortProfileInventoryFromPayloads(portInfoPayload: unknown, packageListPayload: unknown): DsmPortProfileInventory | undefined;
42
42
  /**
@@ -136,7 +136,7 @@ export function installedPackageIdentitiesFromPayload(payload) {
136
136
  *
137
137
  * The package list is a forward mapping: package identity -> project name.
138
138
  * Do not use presumedPackageIdentity here; replacing dashes with dots is
139
- * lossy for identities such as com.example.hello-web.
139
+ * lossy for identities such as com.example.container-web.
140
140
  */
141
141
  export function dsmPortProfileInventoryFromPayloads(portInfoPayload, packageListPayload) {
142
142
  const profiles = parseDsmPortInfoPayload(portInfoPayload);
package/dist/init.js CHANGED
@@ -4,8 +4,6 @@ import { templatesRoot } from "./template-paths.js";
4
4
  import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "./build-spk/dsm-version.js";
5
5
  import { readSharedFolder } from "./standalone/shared-folder.js";
6
6
  const TEMPLATE_NAMES = [
7
- "hello-web",
8
- "hello-web-page",
9
7
  "hello-files",
10
8
  "compose-import",
11
9
  "binary",
@@ -13,8 +11,6 @@ const TEMPLATE_NAMES = [
13
11
  ];
14
12
  /** Templates whose App runs through the DSM docker-project resource worker. */
15
13
  const CONTAINER_TEMPLATES = [
16
- "hello-web",
17
- "hello-web-page",
18
14
  "hello-files",
19
15
  "compose-import",
20
16
  ];
@@ -103,6 +99,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
103
99
  const overlays = [
104
100
  ...(rootExample ? [ROOT_OVERLAY] : []),
105
101
  ...(ui === "vue" ? ["native-go-vue"] : []),
102
+ ...(privilege === "root" ? ["native-go-authorization"] : []),
106
103
  ...(options.background ? [BACKGROUND_OVERLAY] : []),
107
104
  ...(options.integration ? ["native-go-integration"] : []),
108
105
  ...(options.integration === "better-auth" ? ["native-go-better-auth"] : []),
@@ -128,6 +125,8 @@ export function initFromTemplate(template, targetDir, options = {}) {
128
125
  }
129
126
  if (options.integration === "better-auth")
130
127
  cpSync(join(shared, 'notice-overrides'), join(targetDir, 'node/notice-overrides'), { recursive: true });
128
+ if (privilege === "root")
129
+ rmSync(join(targetDir, "backend/internal/app/authorization_none.go"));
131
130
  if (rootExample)
132
131
  ROOT_REPLACED.forEach(file => rmSync(join(targetDir, file)));
133
132
  if (options.background)
@@ -154,6 +153,28 @@ export function initFromTemplate(template, targetDir, options = {}) {
154
153
  writeFileSync(manifest, readFileSync(manifest, "utf8").replace(" dir: ui", " dir: frontend/dist\n build: {cwd: frontend, command: [npm, run, build]}"));
155
154
  rmSync(join(targetDir, "ui"), { recursive: true });
156
155
  }
156
+ if (privilege === "root" && ui === "vue") {
157
+ for (const entry of ["App.vue", "SDKApp.vue"]) {
158
+ const app = join(targetDir, "frontend/src", entry);
159
+ writeFileSync(app, readFileSync(app, "utf8")
160
+ .replace("import AppShell from './shell/AppShell.vue'", "import AuthorizationGate from './authorization/AuthorizationGate.vue'\nimport AppShell from './shell/AppShell.vue'")
161
+ .replace('<AppShell v-model="page"', '<AuthorizationGate :ctx="ctx" @ready="authorizationReady"><AppShell v-model="page"')
162
+ .replace('onMounted(async()=>{\n props.ctx.reopen(()=>{if(!disposed) void refresh()})\n await refresh()', 'async function authorizationReady(){\n await refresh()')
163
+ .replace('})\nonUnmounted', '}\nonMounted(()=>{if(!props.ctx.authorization){props.ctx.reopen(()=>{if(!disposed) void refresh()});void authorizationReady()}})\nonUnmounted')
164
+ .replace('</AppShell>', '</AppShell></AuthorizationGate>'));
165
+ }
166
+ for (const entry of ["main.ts", "sdk-main.ts"]) {
167
+ const main = join(targetDir, "frontend/src", entry);
168
+ writeFileSync(main, readFileSync(main, "utf8").replace('{mount: typeof mount}', '{mount: typeof mount; authorizationUI?: boolean}').replace('{mount}', '{mount, authorizationUI: true}'));
169
+ }
170
+ for (const file of ["apps.test.ts", "nas-ui.test.ts", "shell-ui.test.ts", "shell-sdk.test.ts", "picker-wire.test.ts"]) {
171
+ const test = join(targetDir, "frontend/test", file);
172
+ const source = readFileSync(test, "utf8");
173
+ writeFileSync(test, source + "\n// Test the application surface independently; authorization has its own contract suite.\nvi.mock('../src/authorization/AuthorizationGate.vue',()=>({default:{template:'<slot />'}}))\n");
174
+ }
175
+ }
176
+ if (ui !== "vue")
177
+ rmSync(join(targetDir, "frontend"), { recursive: true, force: true });
157
178
  if (options.background) {
158
179
  const manifest = join(targetDir, "dsm-package.yaml");
159
180
  writeFileSync(manifest, readFileSync(manifest, "utf8") + "\nbackground:\n worker: sha256-v1\n");
@@ -0,0 +1,2 @@
1
+ /** Shared host flow. CGI plans are never accepted as completed authorization. */
2
+ export declare const AUTHORIZATION_RUNTIME = "function createAuthorizationAccess(options) {\n var busy = null;\n function fail(code) { var error = new Error(code); error.code = code; return error; }\n function status() { return options.call(\"authorization.status\").then(function (d) {\n if (!d || d.command !== \"authorization\" || d.schema_version !== 1 || [\"ready\",\"authorization_required\",\"not_admin\",\"session_expired\",\"unavailable\"].indexOf(d.state) < 0) throw fail(\"authorization_bad_response\");\n return d;\n }); }\n function plan(method) { return options.call(method).then(function (p) {\n if (!p || p.command !== \"authorization\" || p.schema_version !== 1 || p.phase !== \"plan\" || !Array.isArray(p.operations) || p.operations.length === 0 || p.operations.length > options.tasks.length) throw fail(\"authorization_bad_response\");\n var seen = {};\n p.operations.forEach(function (op) { if (seen[op] || !options.tasks.some(function (t) { return t.op === op; })) throw fail(\"authorization_bad_response\"); seen[op] = true; });\n return p;\n }); }\n function exclusive(fn) { if (busy) return busy; busy = Promise.resolve().then(fn).then(function (s) { busy = null; return s; },function (e) { busy = null; throw e; });return busy; }\n function authorize(ops,run) { return ops.reduce(function (previous,op) { return previous.then(function () {\n return options.root.authorize(op).then(function (state) { if (state !== \"ready\") throw fail(\"root_cancelled\"); return run ? options.root.run(op) : null; });\n }); },Promise.resolve()); }\n return {canManage:function () {return !!options.user && options.user().admin === true;},status:status,request:function () { return exclusive(function () {\n return plan(\"authorization.request\").then(function (p) { return authorize(p.operations,p.run === true); }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); },revoke:function () { return exclusive(function () {\n return plan(\"authorization.revoke\").then(function (p) {\n if (p.withdraw === true && typeof options.withdraw !== \"function\") throw fail(\"authorization_bad_response\");\n var disabled = p.withdraw === true ? options.withdraw() : p.disable === true ? authorize([\"helper_disable\"],true) : Promise.resolve();\n return disabled.then(function () { return p.purge === true ? authorize([\"helper_purge\"],true) : null; }).then(function () { return p.operations.reduce(function (previous,op) {return previous.then(function () {return options.root.remove(op);});},Promise.resolve()); });\n }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); }};\n}";
@@ -0,0 +1,28 @@
1
+ /** Shared host flow. CGI plans are never accepted as completed authorization. */
2
+ export const AUTHORIZATION_RUNTIME = `function createAuthorizationAccess(options) {
3
+ var busy = null;
4
+ function fail(code) { var error = new Error(code); error.code = code; return error; }
5
+ function status() { return options.call("authorization.status").then(function (d) {
6
+ if (!d || d.command !== "authorization" || d.schema_version !== 1 || ["ready","authorization_required","not_admin","session_expired","unavailable"].indexOf(d.state) < 0) throw fail("authorization_bad_response");
7
+ return d;
8
+ }); }
9
+ function plan(method) { return options.call(method).then(function (p) {
10
+ if (!p || p.command !== "authorization" || p.schema_version !== 1 || p.phase !== "plan" || !Array.isArray(p.operations) || p.operations.length === 0 || p.operations.length > options.tasks.length) throw fail("authorization_bad_response");
11
+ var seen = {};
12
+ p.operations.forEach(function (op) { if (seen[op] || !options.tasks.some(function (t) { return t.op === op; })) throw fail("authorization_bad_response"); seen[op] = true; });
13
+ return p;
14
+ }); }
15
+ function exclusive(fn) { if (busy) return busy; busy = Promise.resolve().then(fn).then(function (s) { busy = null; return s; },function (e) { busy = null; throw e; });return busy; }
16
+ function authorize(ops,run) { return ops.reduce(function (previous,op) { return previous.then(function () {
17
+ return options.root.authorize(op).then(function (state) { if (state !== "ready") throw fail("root_cancelled"); return run ? options.root.run(op) : null; });
18
+ }); },Promise.resolve()); }
19
+ return {canManage:function () {return !!options.user && options.user().admin === true;},status:status,request:function () { return exclusive(function () {
20
+ return plan("authorization.request").then(function (p) { return authorize(p.operations,p.run === true); }).then(status,function (e) { if (e.code === "root_cancelled") return status();throw e; });
21
+ }); },revoke:function () { return exclusive(function () {
22
+ return plan("authorization.revoke").then(function (p) {
23
+ if (p.withdraw === true && typeof options.withdraw !== "function") throw fail("authorization_bad_response");
24
+ var disabled = p.withdraw === true ? options.withdraw() : p.disable === true ? authorize(["helper_disable"],true) : Promise.resolve();
25
+ return disabled.then(function () { return p.purge === true ? authorize(["helper_purge"],true) : null; }).then(function () { return p.operations.reduce(function (previous,op) {return previous.then(function () {return options.root.remove(op);});},Promise.resolve()); });
26
+ }).then(status,function (e) { if (e.code === "root_cancelled") return status();throw e; });
27
+ }); }};
28
+ }`;
@@ -0,0 +1,48 @@
1
+ import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
2
+ import type { CommandRunner } from './build.js';
3
+ import { type RootOp } from './root-ops.js';
4
+ import type { StandaloneManifest } from './manifest.js';
5
+ export interface PinnedFile {
6
+ path: string;
7
+ size: number;
8
+ sha256: string;
9
+ }
10
+ export interface AuthorizationInventory {
11
+ schema: 2;
12
+ package: string;
13
+ version: string;
14
+ arch: string;
15
+ operations: RootOp[];
16
+ files: PinnedFile[];
17
+ digest: string;
18
+ }
19
+ export interface AuthorizationArtifact {
20
+ binary: string;
21
+ inventory: AuthorizationInventory;
22
+ bootstrapSha256: string;
23
+ ops: RootOp[];
24
+ }
25
+ export declare function authorizationInventory(m: {
26
+ package: string;
27
+ spkVersion: string;
28
+ rootOps?: RootOp[];
29
+ }, arch: string, files: PinnedFile[]): AuthorizationInventory;
30
+ export declare function bootstrapOps(m: {
31
+ package: string;
32
+ spkVersion: string;
33
+ rootOps?: RootOp[];
34
+ }, pin: string): RootOp[];
35
+ export declare function authorizationPlan(a: AuthorizationArtifact, m: {
36
+ package: string;
37
+ spkVersion: string;
38
+ }): {
39
+ schema: number;
40
+ package: string;
41
+ version: string;
42
+ bootstrapSha256: string;
43
+ inventory: AuthorizationInventory;
44
+ tasks: import("./root-window.js").RootTask[];
45
+ };
46
+ export declare function buildAuthorization(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): AuthorizationArtifact | undefined;
47
+ /** Archive consistency, not publisher authenticity or device runtime certification. */
48
+ export declare function inspectAuthorization(dir: string, pkg: string, version: string): string[];
@@ -0,0 +1,116 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join, relative } from 'node:path';
4
+ import { renderRootTaskScript } from './root-ops.js';
5
+ import { rootTasks } from './root-window.js';
6
+ import { StandaloneBuildError } from './spk.js';
7
+ import { assertHelperTaskBudget } from './helper.js';
8
+ const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
9
+ export function authorizationInventory(m, arch, files) {
10
+ const base = { schema: 2, package: m.package, version: m.spkVersion, arch, operations: m.rootOps ?? [], files: [...files].sort((a, b) => a.path.localeCompare(b.path, 'en')) };
11
+ return { ...base, digest: hash(JSON.stringify(base)) };
12
+ }
13
+ export function bootstrapOps(m, pin) {
14
+ if (!/^[a-f0-9]{64}$/.test(pin))
15
+ throw new StandaloneBuildError('AUTHORIZATION_PIN_REQUIRED', 'bootstrap SHA-256 pin required');
16
+ return (m.rootOps ?? []).map(op => ({ name: op.name, protectedOutput: true, exposeOutput: op.protectedOutput !== true, body: `#!/bin/sh
17
+ set -eu
18
+ umask 077
19
+ [ "$(id -u)" = 0 ]
20
+ info=/var/packages/${m.package}/INFO
21
+ [ ! -L "$info" ] && [ "$(stat -c %u "$info")" = 0 ]
22
+ grep -Fqx 'version="${m.spkVersion}"' "$info"
23
+ for p in /var /var/lib; do
24
+ [ ! -L "$p" ] && [ -d "$p" ] && [ "$(stat -c %u "$p")" = 0 ]
25
+ mode=$(stat -c %a "$p"); [ "$((0$mode & 0022))" = 0 ]
26
+ done
27
+ s=/var/lib/synopkgland-auth-${m.package}
28
+ [ ! -L "$s" ]
29
+ if [ ! -e "$s" ]; then mkdir -m 0700 "$s"; fi
30
+ [ -d "$s" ] && [ "$(stat -c %u:%a "$s")" = 0:700 ]
31
+ tmp=$(mktemp "$s/.bootstrap.XXXXXX")
32
+ trap 'rm -f "$tmp"; rmdir "$s" 2>/dev/null || :' EXIT HUP INT TERM
33
+ dd if=/var/packages/${m.package}/target/authorization/bootstrap of="$tmp" bs=65536 count=1025 2>/dev/null
34
+ [ "$(stat -c %s "$tmp")" -le 67108864 ]
35
+ chmod 0700 "$tmp"
36
+ [ "$(sha256sum "$tmp" | cut -d ' ' -f 1)" = '${pin}' ]
37
+ "$tmp" '${op.name}'
38
+ ` }));
39
+ }
40
+ export function authorizationPlan(a, m) {
41
+ return { schema: 2, package: m.package, version: m.spkVersion, bootstrapSha256: a.bootstrapSha256, inventory: a.inventory, tasks: rootTasks({ ...m, rootOps: a.ops }) };
42
+ }
43
+ export function buildAuthorization(m, arch, work, run) {
44
+ if (m.privilege !== 'root')
45
+ return undefined;
46
+ if (m.backend.kind !== 'go')
47
+ throw new StandaloneBuildError('AUTHORIZATION_GO_REQUIRED', 'root authorization requires the native-Go CGI template');
48
+ const files = [];
49
+ const add = (path, source) => { const bytes = readFileSync(source); files.push({ path, size: bytes.length, sha256: hash(bytes) }); };
50
+ const walk = (dir) => { for (const name of readdirSync(dir).sort()) {
51
+ const p = join(dir, name), st = lstatSync(p);
52
+ if (st.isDirectory())
53
+ walk(p);
54
+ else if (st.isFile())
55
+ add('ui/app/' + relative(m.ui.dir, p).split('\\').join('/'), p);
56
+ else
57
+ throw new StandaloneBuildError('AUTHORIZATION_PAYLOAD_UNSAFE', 'UI inventory requires regular files');
58
+ } };
59
+ walk(m.ui.dir);
60
+ // Helper bodies already contain the exact helper digest; worker payloads are
61
+ // bound by its compiled registry. UI bytes are additionally checked at launch.
62
+ const inventory = authorizationInventory(m, arch, files);
63
+ const dir = join(work, 'authorization-' + arch);
64
+ mkdirSync(dir);
65
+ const source = readFileSync(new URL('../../templates/_authorization/main.go', import.meta.url), 'utf8');
66
+ writeFileSync(join(dir, 'main.go'), source);
67
+ const binary = join(work, 'bootstrap-' + arch);
68
+ const compiled = JSON.stringify({ ...inventory, operations: inventory.operations.map(op => ({ ...op, body: op.body })) });
69
+ const result = run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', `-s -w -buildid= -X 'main.inventoryBase64=${Buffer.from(compiled).toString('base64')}'`, '-o', binary, 'main.go'], { cwd: dir, env: { ...process.env, CGO_ENABLED: '0', GOOS: 'linux', GOARCH: arch === 'x86_64' ? 'amd64' : 'arm64' } });
70
+ if (result.missing || result.status !== 0)
71
+ throw new StandaloneBuildError('AUTHORIZATION_BUILD_FAILED', result.stderr);
72
+ const bootstrapSha256 = hash(readFileSync(binary));
73
+ const ops = bootstrapOps(m, bootstrapSha256);
74
+ ops.forEach(op => assertHelperTaskBudget(renderRootTaskScript(m, op)));
75
+ return { binary, inventory, bootstrapSha256, ops };
76
+ }
77
+ /** Archive consistency, not publisher authenticity or device runtime certification. */
78
+ export function inspectAuthorization(dir, pkg, version) {
79
+ if (!existsSync(join(dir, 'authorization/manifest.json')))
80
+ return ['root authorization manifest missing'];
81
+ try {
82
+ const plan = JSON.parse(readFileSync(join(dir, 'authorization/manifest.json'), 'utf8'));
83
+ if (plan.schema !== 2 || plan.package !== pkg || plan.version !== version || !/^[a-f0-9]{64}$/.test(plan.bootstrapSha256))
84
+ throw Error();
85
+ const bytes = readFileSync(join(dir, 'authorization/bootstrap'));
86
+ if (hash(bytes) !== plan.bootstrapSha256 || !bytes.includes(Buffer.from(Buffer.from(JSON.stringify(plan.inventory)).toString('base64'))))
87
+ throw Error();
88
+ const inventory = authorizationInventory({ package: pkg, spkVersion: version, rootOps: plan.inventory.operations }, plan.inventory.arch, plan.inventory.files);
89
+ if (JSON.stringify(inventory) !== JSON.stringify(plan.inventory))
90
+ throw Error();
91
+ if (JSON.stringify(plan.tasks) !== JSON.stringify(rootTasks({ package: pkg, spkVersion: version, rootOps: bootstrapOps({ package: pkg, spkVersion: version, rootOps: inventory.operations }, plan.bootstrapSha256) })))
92
+ throw Error();
93
+ const seen = new Set();
94
+ for (const f of inventory.files) {
95
+ if (!/^ui\/app\/(?!.*(?:^|\/)\.\.(?:\/|$))[^\x00\\]+$/.test(f.path) || seen.has(f.path))
96
+ throw Error();
97
+ seen.add(f.path);
98
+ const b = readFileSync(join(dir, f.path));
99
+ if (b.length !== f.size || hash(b) !== f.sha256)
100
+ throw Error();
101
+ }
102
+ const cgi = readFileSync(join(dir, 'ui/cgi/api.cgi'));
103
+ if (!cgi.includes(Buffer.from(Buffer.from(JSON.stringify(plan)).toString('base64'))))
104
+ throw Error();
105
+ const windows = readdirSync(join(dir, 'ui')).filter(f => /^window-\d+\.js$/.test(f));
106
+ if (windows.length !== 1)
107
+ throw Error();
108
+ const window = readFileSync(join(dir, 'ui', windows[0]), 'utf8');
109
+ if (!window.includes(JSON.stringify(plan.tasks)))
110
+ throw Error();
111
+ return [];
112
+ }
113
+ catch {
114
+ return ['root authorization bootstrap pin, inventory, CGI or task binding invalid'];
115
+ }
116
+ }
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "../build-spk/dsm-version.js";
@@ -8,6 +8,7 @@ import { loadStandaloneManifest } from "./manifest.js";
8
8
  import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
9
9
  import { buildHelper } from "./helper.js";
10
10
  import { buildDaemonControl } from "./daemon.js";
11
+ import { buildAuthorization, authorizationPlan } from "./authorization.js";
11
12
  import { BACKGROUND_WORKER_MAIN } from "./background.js";
12
13
  const GOARCH = { x86_64: "amd64", aarch64: "arm64" };
13
14
  /** Argv only, never a shell; output streams to the terminal except stderr, which is also kept. */
@@ -61,16 +62,26 @@ export async function buildStandalone(options) {
61
62
  if (problem)
62
63
  throw new StandaloneBuildError(problem.code, problem.message);
63
64
  }
64
- const cgiPath = backendFor(manifest, arch, work, run, helper);
65
+ const operationsManifest = helper ? { ...manifest, rootOps: [...(manifest.rootOps ?? []), ...helper.ops] } : manifest;
66
+ const authorization = buildAuthorization(operationsManifest, arch, work, run);
67
+ if (authorization) {
68
+ const problem = checkCgiBinary(work, authorization.binary, arch);
69
+ if (problem)
70
+ throw new StandaloneBuildError(problem.code, problem.message);
71
+ }
72
+ const cgiPath = backendFor(manifest, arch, work, run, helper, authorization);
65
73
  const problem = checkCgiBinary(manifest.backend.kind === "go" ? work : manifest.root, cgiPath, arch);
66
74
  if (problem) {
67
75
  throw new StandaloneBuildError(problem.code, problem.message);
68
76
  }
77
+ if (authorization && !readFileSync(cgiPath).includes(Buffer.from(JSON.stringify(authorizationPlan(authorization, manifest))).toString("base64"))) {
78
+ throw new StandaloneBuildError("AUTHORIZATION_CGI_REQUIRED", "root CGI must contain its compiled authorization plan; regenerate the root template adapter");
79
+ }
69
80
  const outputPath = join(outDir, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
70
81
  const daemonControl = buildDaemonControl(manifest, arch, work, run);
71
- const builtManifest = helper ? { ...manifest, rootOps: [...(manifest.rootOps ?? []), ...helper.ops] } : manifest;
82
+ const builtManifest = authorization ? { ...manifest, rootOps: authorization.ops } : operationsManifest;
72
83
  const backgroundWorker = buildBackgroundWorker(manifest, arch, work, run);
73
- await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, helper, daemonControl, backgroundWorker });
84
+ await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, helper, daemonControl, backgroundWorker, authorization });
74
85
  outputs.push(outputPath);
75
86
  }
76
87
  return { outputs };
@@ -93,7 +104,7 @@ function buildUi(manifest, run) {
93
104
  throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build(${build.command.join(" ")})失敗。${installHint}\n${result.stderr}`);
94
105
  }
95
106
  }
96
- function backendFor(manifest, arch, work, run, helper) {
107
+ function backendFor(manifest, arch, work, run, helper, authorization) {
97
108
  const backend = manifest.backend;
98
109
  if (backend.kind === "binary") {
99
110
  return backend.files[arch];
@@ -106,6 +117,7 @@ function backendFor(manifest, arch, work, run, helper) {
106
117
  `-X main.packageID=${manifest.package}`,
107
118
  `-X main.version=${manifest.spkVersion}`,
108
119
  ...(helper ? [`-X 'main.helperInventory=${JSON.stringify(helper.inventory)}'`] : []),
120
+ ...(authorization ? [`-X 'main.rootAuthorization=${Buffer.from(JSON.stringify(authorizationPlan(authorization, manifest))).toString('base64')}'`] : []),
109
121
  // Integration without a helper: the CGI serves its package-account features itself.
110
122
  ...(manifest.integration && !helper ? ["-X main.integrationMode=package"] : []),
111
123
  ...(manifest.fileStation ? [`-X main.fileStation=${manifest.fileStation}`] : []),
@@ -115,7 +127,7 @@ function backendFor(manifest, arch, work, run, helper) {
115
127
  env: { ...process.env, CGO_ENABLED: "0", GOOS: "linux", GOARCH: GOARCH[arch] },
116
128
  });
117
129
  if (result.missing) {
118
- throw new StandaloneBuildError("STANDALONE_GO_MISSING", "找不到 go 指令。請安裝 Go 1.24 以上版本(https://go.dev/dl/),或改用 backend.binary 打包既有執行檔。");
130
+ throw new StandaloneBuildError("STANDALONE_GO_MISSING", "找不到 go 指令。請安裝 Go 1.25 以上版本(https://go.dev/dl/),或改用 backend.binary 打包既有執行檔。");
119
131
  }
120
132
  if (result.status !== 0) {
121
133
  throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `go build(${arch})失敗。\n${result.stderr}`);
@@ -1,3 +1,4 @@
1
+ import { AUTHORIZATION_RUNTIME } from "./authorization-window.js";
1
2
  import { REOPEN_EVENTS_RUNTIME } from "./reopen-events.js";
2
3
  import { DSM_LANGS } from "./i18n.js";
3
4
  import { ROOT_MESSAGES } from "./root-messages.js";
@@ -180,15 +181,17 @@ ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
180
181
  var i18n = ${ns}.createI18n(lang, ${ns}.messages);
181
182
  me.hostAccess = ${ns}.createHost({ appWindow: me, locale: i18n.locale });
182
183
  root.lang = i18n.locale;
184
+ if (app.authorizationUI && me.body) { var panels = me.body.dom.querySelector(".dsmapp-rootpanels"); if (panels) panels.hidden = true; }
183
185
  try {
184
186
  me.mounted = app.mount(root, {
187
+ displayName: ${JSON.stringify(manifest.displayname)},
185
188
  call: ${ns}.call,
186
189
  reopen: me.reopenEvents.subscribe,
187
190
  host: me.hostAccess,
188
191
  appWindow: me,
189
192
  lang: lang,
190
193
  locale: i18n.locale,
191
- t: i18n.t${root ? ",\n root: access" : ""}
194
+ t: i18n.t${root ? `,\n root: access,\n authorization: ${ns}.createAuthorizationAccess({root: access, call: ${ns}.call, tasks: ${ns}.rootTasks, user: session})` : ""}
192
195
  }) || {};
193
196
  } catch (error) {
194
197
  me.hostAccess.dispose();
@@ -239,6 +242,10 @@ const ROOT_PANEL_CSS = [
239
242
  function renderRootParts(ns, manifest) {
240
243
  return {
241
244
  definitions: `${ns}.rootTasks = ${JSON.stringify(rootTasks(manifest))};
245
+ ${ns}.createAuthorizationAccess = (function () {
246
+ ${AUTHORIZATION_RUNTIME}
247
+ return createAuthorizationAccess;
248
+ })();
242
249
  ${ns}.rootMessages = ${JSON.stringify(ROOT_MESSAGES)};
243
250
  ${ns}.createRootAccess = (function () {
244
251
  ${ROOT_RUNTIME}
@@ -57,7 +57,7 @@ export function assertHelperTaskBudget(script, metadata = JSON.stringify({ scrip
57
57
  throw new StandaloneBuildError("HELPER_METADATA_TOO_LARGE", "helper scheduler metadata 超過 8191 bytes 有界限制。");
58
58
  }
59
59
  export function helperOps(m, inventory) {
60
- return ["enable", "recover", "teardown", "disable", "repair"].map(action => {
60
+ return ["enable", "recover", "teardown", "disable", "repair", ...(m.privilege === 'root' ? ['purge'] : [])].map(action => {
61
61
  const op = { name: `helper_${action}`, protectedOutput: true, body: `#!/bin/sh
62
62
  set -eu
63
63
  umask 077
@@ -1,3 +1,4 @@
1
+ import { bootstrapOps } from "./authorization.js";
1
2
  import { readFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import * as tar from "tar";
@@ -18,7 +19,7 @@ export async function inspectHelper(dir, info, findings, daemon = false) {
18
19
  // Enumerate candidates to reject extras; only the INFO-derived exact path is read.
19
20
  if (/^ui\/window-[^/]*\.js$/.test(path))
20
21
  windows.push(path);
21
- if (entry.type !== "File" || !(path.startsWith("integration/workers/") || ["integration/helper", "integration/inventory.json", "integration/workers.json", "ui/cgi/api.cgi", windowPath].includes(path)))
22
+ if (entry.type !== "File" || !(path.startsWith("integration/workers/") || ["authorization/manifest.json", "integration/helper", "integration/inventory.json", "integration/workers.json", "ui/cgi/api.cgi", windowPath].includes(path)))
22
23
  return;
23
24
  const limit = ["integration/helper", "ui/cgi/api.cgi"].includes(path) ? 64 * 1024 * 1024 : 1024 * 1024;
24
25
  if (files.has(path) || entry.size > limit) {
@@ -75,7 +76,16 @@ export async function inspectHelper(dir, info, findings, daemon = false) {
75
76
  throw new Error("unregistered worker");
76
77
  const manifest = { package: i.package, spkVersion: i.version, privilege: "root", integration: { protocol: i.protocol, workers: {} } };
77
78
  const tasks = JSON.parse(/\.rootTasks = (\[[^\n]+\]);/.exec(bytes(windowPath).toString("utf8"))?.[1] ?? "null");
78
- const expected = rootTasks({ ...manifest, rootOps: helperOps(manifest, i) });
79
+ const originalOps = helperOps(manifest, i);
80
+ let expected = rootTasks({ ...manifest, rootOps: originalOps });
81
+ // Existing CLI 0.5 archives have inline hash-pinned helper tasks. New
82
+ // bootstrap archives carry an additional independently inspected inventory.
83
+ if (files.has("authorization/manifest.json")) {
84
+ const plan = JSON.parse(bytes("authorization/manifest.json").toString("utf8"));
85
+ if (JSON.stringify(plan.inventory.operations.filter((op) => op.name.startsWith("helper_"))) !== JSON.stringify(originalOps))
86
+ throw new Error("bootstrap helper binding mismatch");
87
+ expected = rootTasks({ ...manifest, rootOps: bootstrapOps({ ...manifest, rootOps: originalOps }, plan.bootstrapSha256) });
88
+ }
79
89
  if (!Array.isArray(tasks) || JSON.stringify(tasks.filter(t => t.op?.startsWith("helper_"))) !== JSON.stringify(expected))
80
90
  throw new Error("approved task mismatch");
81
91
  // Daemon lifecycle is verified separately with the complete daemon config.
@@ -1,3 +1,4 @@
1
+ import { inspectAuthorization } from "./authorization.js";
1
2
  import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
@@ -77,6 +78,16 @@ export async function inspectSpk(spkPath, expectedKind) {
77
78
  else if (integrationPaths.some(path => path !== "integration/identity/app.mjs")) {
78
79
  findings.push("integration 沒有 helper 時只可附 integration/identity/app.mjs");
79
80
  }
81
+ if (info.sdk_root_authorization === "2" || [...paths].some(path => path.startsWith("authorization/"))) {
82
+ if (info.sdk_root_authorization !== "2")
83
+ findings.push("root authorization INFO marker missing");
84
+ const authDir = join(dir, "authorization-inspection");
85
+ mkdirSync(authDir);
86
+ await tar.x({ file: join(dir, "package.tgz"), cwd: authDir });
87
+ findings.push(...inspectAuthorization(authDir, info.package, info.version));
88
+ if (payload.find(p => p.path === "authorization/bootstrap")?.mode !== "0755")
89
+ findings.push("authorization bootstrap must be executable");
90
+ }
80
91
  const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
81
92
  if (kind === "native-go" && !cgiEntry) {
82
93
  findings.push(`payload 沒有 ${CGI_PAYLOAD_PATH}`);
@@ -1,2 +1,2 @@
1
1
  /** Non-privileged management veto shared by the generated package hooks. */
2
- export declare function renderHelperStoppedCheck(packageId: string): string;
2
+ export declare function renderHelperStoppedCheck(packageId: string, revoke?: boolean): string;
@@ -1,6 +1,6 @@
1
1
  /** Non-privileged management veto shared by the generated package hooks. */
2
- export function renderHelperStoppedCheck(packageId) {
3
- return `/var/packages/${packageId}/target/integration/helper check-stopped || {
2
+ export function renderHelperStoppedCheck(packageId, revoke = false) {
3
+ return `${revoke ? `/var/packages/${packageId}/target/integration/helper revoke && ` : ''}/var/packages/${packageId}/target/integration/helper check-stopped || {
4
4
  message='Run the approved helper_teardown or helper_disable operation before stopping, upgrading or uninstalling.'
5
5
  printf '%s\\n' "$message" >&2
6
6
  if [ -n "\${SYNOPKG_TEMP_LOGFILE:-}" ]; then
@@ -1,3 +1,3 @@
1
1
  import type { StandaloneManifest } from "./manifest.js";
2
- /** Non-privileged hooks: precheck gates DSM management; no hook tears down root. */
2
+ /** Package-UID hooks: root auth withdraws via the trusted socket, then retains the stopped veto. */
3
3
  export declare function renderStandaloneScript(name: string, manifest: StandaloneManifest): string;
@@ -3,7 +3,7 @@ import { renderHelperStoppedCheck } from "./lifecycle-helper.js";
3
3
  import { renderBackgroundControl } from "./background.js";
4
4
  import { renderPackagePostuninst } from "../build-spk/native-lifecycle.js";
5
5
  import { renderSharedFolderScript } from "./shared-folder.js";
6
- /** Non-privileged hooks: precheck gates DSM management; no hook tears down root. */
6
+ /** Package-UID hooks: root auth withdraws via the trusted socket, then retains the stopped veto. */
7
7
  export function renderStandaloneScript(name, manifest) {
8
8
  if (name === "postuninst")
9
9
  return renderPackagePostuninst(manifest.package);
@@ -14,7 +14,7 @@ export function renderStandaloneScript(name, manifest) {
14
14
  if (manifest.background && name === "start-stop-status")
15
15
  return renderBackgroundControl(manifest.package);
16
16
  if (hasHelper(manifest) && ["preupgrade", "preuninst", "start-stop-status"].includes(name)) {
17
- const check = renderHelperStoppedCheck(manifest.package);
17
+ const check = renderHelperStoppedCheck(manifest.package, manifest.privilege === 'root');
18
18
  if (name !== "start-stop-status")
19
19
  return `#!/bin/sh\n${check}`;
20
20
  return `#!/bin/sh
@@ -44,7 +44,7 @@ function renderDaemonScript(name, manifest) {
44
44
  const pkg = manifest.package;
45
45
  const control = `"/var/packages/${pkg}/target/daemon/control"`;
46
46
  const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}"`;
47
- const check = hasHelper(manifest) ? renderHelperStoppedCheck(pkg) : "";
47
+ const check = hasHelper(manifest) ? renderHelperStoppedCheck(pkg, manifest.privilege === 'root') : "";
48
48
  if (["preupgrade", "preuninst"].includes(name))
49
49
  return `#!/bin/sh\n${check}exec ${control} ${name === "preuninst" && manifest.daemon?.uninstall_args ? "uninstall" : "stop"} ${paths}\n`;
50
50
  if (name !== "start-stop-status")
@@ -11,6 +11,8 @@ export interface RootOp {
11
11
  body: string;
12
12
  /** Internal generator flag for protected helper control output. */
13
13
  protectedOutput?: boolean;
14
+ /** Bootstrap wraps public output in a protected directory without hiding it. */
15
+ exposeOutput?: boolean;
14
16
  }
15
17
  export type RootOpsResult = {
16
18
  ok: true;