@synopackageland/cli 0.3.9 → 0.4.0

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 (286) hide show
  1. package/README.md +20 -9
  2. package/dist/bin.js +1 -2
  3. package/dist/build-spk/generator.d.ts +0 -3
  4. package/dist/build-spk/generator.js +135 -287
  5. package/dist/build-spk/native-lifecycle.js +0 -3
  6. package/dist/build-spk/native-payload.js +0 -1
  7. package/dist/cli.js +171 -151
  8. package/dist/compose/analyze.d.ts +1 -2
  9. package/dist/compose/analyze.js +5 -48
  10. package/dist/contract/unsupported-types.d.ts +10 -0
  11. package/dist/contract/unsupported-types.js +49 -0
  12. package/dist/contract/validate.js +25 -2
  13. package/dist/contract/web-launch.js +0 -2
  14. package/dist/deploy/deploy.d.ts +1 -4
  15. package/dist/deploy/deploy.js +57 -156
  16. package/dist/deploy/homes-values.js +1 -1
  17. package/dist/deploy/plan.d.ts +18 -0
  18. package/dist/deploy/plan.js +54 -0
  19. package/dist/deploy/port-preflight.d.ts +21 -0
  20. package/dist/deploy/port-preflight.js +42 -0
  21. package/dist/deploy/spk-version.d.ts +2 -14
  22. package/dist/deploy/spk-version.js +25 -111
  23. package/dist/dev.d.ts +1 -3
  24. package/dist/dev.js +14 -28
  25. package/dist/error-registry.js +810 -857
  26. package/dist/host/dsm-client.js +23 -263
  27. package/dist/host/dsm-port-profile.d.ts +47 -0
  28. package/dist/host/dsm-port-profile.js +206 -0
  29. package/dist/host/port-info-lookup.d.ts +9 -0
  30. package/dist/host/port-info-lookup.js +87 -0
  31. package/dist/host/port-info.d.ts +33 -0
  32. package/dist/host/port-info.js +66 -0
  33. package/dist/host/types.d.ts +6 -78
  34. package/dist/index.d.ts +0 -9
  35. package/dist/index.js +0 -6
  36. package/dist/info.d.ts +0 -1
  37. package/dist/info.js +3 -10
  38. package/dist/init.d.ts +11 -1
  39. package/dist/init.js +112 -4
  40. package/dist/log.js +2 -3
  41. package/dist/native/validate.js +0 -1
  42. package/dist/standalone/binary.d.ts +9 -0
  43. package/dist/standalone/binary.js +67 -0
  44. package/dist/standalone/build.d.ts +25 -0
  45. package/dist/standalone/build.js +108 -0
  46. package/dist/standalone/dsm-window.d.ts +26 -0
  47. package/dist/standalone/dsm-window.js +251 -0
  48. package/dist/standalone/helper.d.ts +31 -0
  49. package/dist/standalone/helper.js +104 -0
  50. package/dist/standalone/i18n.d.ts +28 -0
  51. package/dist/standalone/i18n.js +161 -0
  52. package/dist/standalone/inspect-spk.d.ts +21 -0
  53. package/dist/standalone/inspect-spk.js +160 -0
  54. package/dist/standalone/manifest.d.ts +71 -0
  55. package/dist/standalone/manifest.js +276 -0
  56. package/dist/standalone/root-messages.d.ts +7 -0
  57. package/dist/standalone/root-messages.js +250 -0
  58. package/dist/standalone/root-ops.d.ts +42 -0
  59. package/dist/standalone/root-ops.js +101 -0
  60. package/dist/standalone/root-window.d.ts +35 -0
  61. package/dist/standalone/root-window.js +290 -0
  62. package/dist/standalone/spk.d.ts +22 -0
  63. package/dist/standalone/spk.js +205 -0
  64. package/dist/templates/binary/synology-app.yaml +7 -0
  65. package/dist/templates/hello-files/compose.yaml +0 -1
  66. package/dist/templates/hello-web/compose.yaml +13 -10
  67. package/dist/templates/hello-web/html/app.js +1 -23
  68. package/dist/templates/hello-web/html/index.html +1 -3
  69. package/dist/templates/hello-web-page/compose.yaml +13 -10
  70. package/dist/templates/hello-web-page/html/app.js +1 -23
  71. package/dist/templates/hello-web-page/html/index.html +1 -3
  72. package/dist/templates/native-go/README.md +268 -0
  73. package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
  74. package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
  75. package/dist/templates/native-go/backend/go.mod +3 -0
  76. package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
  77. package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
  78. package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
  79. package/dist/templates/native-go/backend/internal/api/server.go +155 -0
  80. package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
  81. package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
  82. package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
  83. package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
  84. package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  85. package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
  86. package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
  87. package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  88. package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  89. package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  90. package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
  91. package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  92. package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
  93. package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
  94. package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
  95. package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  96. package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  97. package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  98. package/dist/templates/native-go/dsm-package.yaml +58 -0
  99. package/dist/templates/native-go/gitignore +1 -0
  100. package/dist/templates/native-go/i18n/glossary.json +662 -0
  101. package/dist/templates/native-go/i18n/messages/de.json +28 -0
  102. package/dist/templates/native-go/i18n/messages/en.json +28 -0
  103. package/dist/templates/native-go/i18n/messages/es.json +28 -0
  104. package/dist/templates/native-go/i18n/messages/fr.json +28 -0
  105. package/dist/templates/native-go/i18n/messages/it.json +28 -0
  106. package/dist/templates/native-go/i18n/messages/ja.json +28 -0
  107. package/dist/templates/native-go/i18n/messages/ko.json +28 -0
  108. package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
  109. package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
  110. package/dist/templates/native-go/ui/app.css +35 -0
  111. package/dist/templates/native-go/ui/app.js +94 -0
  112. package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  113. package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  114. package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
  115. package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
  116. package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
  117. package/dist/templates/native-go-better-auth/node/package.json +1 -0
  118. package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  119. package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  120. package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  121. package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  122. package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
  123. package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  124. package/dist/templates/native-go-root/root/whoami.sh +6 -0
  125. package/dist/templates/native-go-vue/README.md +110 -0
  126. package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
  127. package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
  128. package/dist/templates/native-go-vue/frontend/package.json +6 -0
  129. package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
  130. package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
  131. package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
  132. package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
  133. package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  134. package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
  135. package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
  136. package/dist/templates/native-go-vue/gitignore +2 -0
  137. package/dist/types.d.ts +1 -1
  138. package/dist/validate.js +11 -7
  139. package/package.json +3 -7
  140. package/schema/synology-app.schema.json +36 -37
  141. package/skills/SKILL.md +4 -48
  142. package/skills/reference/create.md +11 -83
  143. package/skills/reference/deploy.md +7 -78
  144. package/skills/reference/diagnose.md +4 -101
  145. package/skills/reference/files.md +5 -41
  146. package/skills/reference/identity.md +3 -29
  147. package/skills/reference/wrap.md +0 -2
  148. package/templates/binary/synology-app.yaml +7 -0
  149. package/templates/hello-files/compose.yaml +0 -1
  150. package/templates/hello-web/compose.yaml +13 -10
  151. package/templates/hello-web/html/app.js +1 -23
  152. package/templates/hello-web/html/index.html +1 -3
  153. package/templates/hello-web-page/compose.yaml +13 -10
  154. package/templates/hello-web-page/html/app.js +1 -23
  155. package/templates/hello-web-page/html/index.html +1 -3
  156. package/templates/native-go/README.md +268 -0
  157. package/templates/native-go/backend/cmd/app/main.go +49 -0
  158. package/templates/native-go/backend/cmd/helper/main.go +38 -0
  159. package/templates/native-go/backend/go.mod +3 -0
  160. package/templates/native-go/backend/internal/api/errors.go +33 -0
  161. package/templates/native-go/backend/internal/api/registry.go +94 -0
  162. package/templates/native-go/backend/internal/api/registry_test.go +329 -0
  163. package/templates/native-go/backend/internal/api/server.go +155 -0
  164. package/templates/native-go/backend/internal/app/root_none.go +9 -0
  165. package/templates/native-go/backend/internal/app/routes.go +127 -0
  166. package/templates/native-go/backend/internal/app/routes_test.go +167 -0
  167. package/templates/native-go/backend/internal/dsm/auth.go +118 -0
  168. package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  169. package/templates/native-go/backend/internal/helper/control.go +219 -0
  170. package/templates/native-go/backend/internal/helper/control_test.go +242 -0
  171. package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  172. package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  173. package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  174. package/templates/native-go/backend/internal/helper/process_other.go +5 -0
  175. package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  176. package/templates/native-go/backend/internal/helper/status.go +271 -0
  177. package/templates/native-go/backend/internal/helper/status_test.go +205 -0
  178. package/templates/native-go/backend/internal/helper/supervision.go +84 -0
  179. package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  180. package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  181. package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  182. package/templates/native-go/dsm-package.yaml +58 -0
  183. package/templates/native-go/gitignore +1 -0
  184. package/templates/native-go/i18n/glossary.json +662 -0
  185. package/templates/native-go/i18n/messages/de.json +28 -0
  186. package/templates/native-go/i18n/messages/en.json +28 -0
  187. package/templates/native-go/i18n/messages/es.json +28 -0
  188. package/templates/native-go/i18n/messages/fr.json +28 -0
  189. package/templates/native-go/i18n/messages/it.json +28 -0
  190. package/templates/native-go/i18n/messages/ja.json +28 -0
  191. package/templates/native-go/i18n/messages/ko.json +28 -0
  192. package/templates/native-go/i18n/messages/zh-CN.json +28 -0
  193. package/templates/native-go/i18n/messages/zh-TW.json +28 -0
  194. package/templates/native-go/ui/app.css +35 -0
  195. package/templates/native-go/ui/app.js +94 -0
  196. package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  197. package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  198. package/templates/native-go-better-auth/node/app.mjs +72 -0
  199. package/templates/native-go-better-auth/node/build.mjs +2 -0
  200. package/templates/native-go-better-auth/node/package-lock.json +862 -0
  201. package/templates/native-go-better-auth/node/package.json +1 -0
  202. package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  203. package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  204. package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  205. package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  206. package/templates/native-go-root/backend/internal/app/root.go +107 -0
  207. package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  208. package/templates/native-go-root/root/whoami.sh +6 -0
  209. package/templates/native-go-vue/README.md +110 -0
  210. package/templates/native-go-vue/frontend/DESIGN.md +78 -0
  211. package/templates/native-go-vue/frontend/package-lock.json +2507 -0
  212. package/templates/native-go-vue/frontend/package.json +6 -0
  213. package/templates/native-go-vue/frontend/src/App.vue +132 -0
  214. package/templates/native-go-vue/frontend/src/context.ts +17 -0
  215. package/templates/native-go-vue/frontend/src/main.ts +26 -0
  216. package/templates/native-go-vue/frontend/src/style.css +14 -0
  217. package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  218. package/templates/native-go-vue/frontend/tsconfig.json +1 -0
  219. package/templates/native-go-vue/frontend/vite.config.ts +8 -0
  220. package/templates/native-go-vue/gitignore +2 -0
  221. package/dist/attestation/app-package.d.ts +0 -86
  222. package/dist/attestation/app-package.js +0 -206
  223. package/dist/attestation/install-on-host.d.ts +0 -14
  224. package/dist/attestation/install-on-host.js +0 -52
  225. package/dist/attestation/keyring.d.ts +0 -21
  226. package/dist/attestation/keyring.js +0 -168
  227. package/dist/attestation/platform-service.d.ts +0 -70
  228. package/dist/attestation/platform-service.js +0 -250
  229. package/dist/build-spk/broker-glue.d.ts +0 -16
  230. package/dist/build-spk/broker-glue.js +0 -40
  231. package/dist/deploy/catalog-publish.d.ts +0 -3
  232. package/dist/deploy/catalog-publish.js +0 -7
  233. package/dist/doctor.d.ts +0 -22
  234. package/dist/doctor.js +0 -721
  235. package/dist/error-collector.d.ts +0 -2
  236. package/dist/error-collector.js +0 -46
  237. package/dist/host/broker-paths.d.ts +0 -1
  238. package/dist/host/broker-paths.js +0 -1
  239. package/dist/host/broker-upgrade-preflight.d.ts +0 -61
  240. package/dist/host/broker-upgrade-preflight.js +0 -156
  241. package/dist/host/digests.d.ts +0 -5
  242. package/dist/host/digests.js +0 -63
  243. package/dist/host/fake-nas.d.ts +0 -41
  244. package/dist/host/fake-nas.js +0 -174
  245. package/dist/mock-broker/bootstrap.d.ts +0 -2
  246. package/dist/mock-broker/bootstrap.js +0 -8
  247. package/dist/mock-broker/files.d.ts +0 -60
  248. package/dist/mock-broker/files.js +0 -408
  249. package/dist/mock-broker/oidc.d.ts +0 -31
  250. package/dist/mock-broker/oidc.js +0 -161
  251. package/dist/mock-broker/path.d.ts +0 -9
  252. package/dist/mock-broker/path.js +0 -50
  253. package/dist/mock-broker/server.d.ts +0 -19
  254. package/dist/mock-broker/server.js +0 -350
  255. package/dist/publish/build-ledger.d.ts +0 -22
  256. package/dist/publish/build-ledger.js +0 -26
  257. package/dist/publish/community-path.d.ts +0 -6
  258. package/dist/publish/community-path.js +0 -41
  259. package/dist/publish/curated-release.d.ts +0 -78
  260. package/dist/publish/curated-release.js +0 -883
  261. package/dist/publish/file-build-ledger.d.ts +0 -18
  262. package/dist/publish/file-build-ledger.js +0 -268
  263. package/dist/publish/github-pr.d.ts +0 -25
  264. package/dist/publish/github-pr.js +0 -85
  265. package/dist/publish/github-repo.d.ts +0 -8
  266. package/dist/publish/github-repo.js +0 -21
  267. package/dist/publish/platform-service-release.d.ts +0 -60
  268. package/dist/publish/platform-service-release.js +0 -333
  269. package/dist/publish/preflight.d.ts +0 -24
  270. package/dist/publish/preflight.js +0 -138
  271. package/dist/publish/publish.d.ts +0 -27
  272. package/dist/publish/publish.js +0 -136
  273. package/dist/publish/sync-definition.d.ts +0 -4
  274. package/dist/publish/sync-definition.js +0 -48
  275. package/dist/release-bin.d.ts +0 -2
  276. package/dist/release-bin.js +0 -23
  277. package/dist/templates/hello-web/html/sdk.js +0 -68
  278. package/dist/templates/hello-web-page/html/sdk.js +0 -68
  279. package/dist/templates/native/synology-app.yaml +0 -7
  280. package/dist/test-host.d.ts +0 -15
  281. package/dist/test-host.js +0 -37
  282. package/templates/hello-web/html/sdk.js +0 -68
  283. package/templates/hello-web-page/html/sdk.js +0 -68
  284. package/templates/native/synology-app.yaml +0 -7
  285. /package/dist/templates/{native → binary}/native.yaml +0 -0
  286. /package/templates/{native → binary}/native.yaml +0 -0
@@ -0,0 +1,268 @@
1
+ # __DISPLAY_NAME__
2
+
3
+ A native DSM package (`__PACKAGE__`): a Go backend that DSM runs as a CGI,
4
+ and a UI that opens in its own DSM window. It installs as an **unsigned** SPK,
5
+ runs as its own DSM package account, needs no containers, and has no root by default.
6
+
7
+ ```text
8
+ DSM main menu → DSM window (ui/app.js)
9
+ │ ctx.call("greeting.get") same-origin POST + DSM session
10
+ ▼
11
+ /webman/3rdparty/__PACKAGE__/cgi/api.cgi (backend/, runs as package user)
12
+ 1. transport guards: POST, JSON, same origin, ≤ 1 MiB
13
+ 2. one DSM authentication (unless the route is Public)
14
+ 3. route policy: User or Admin → refusal before any handler
15
+ 4. your handler (backend/internal/app)
16
+ ```
17
+
18
+ ## Quick start
19
+
20
+ Needs Node.js with `synopkgland` installed, and Go 1.24+.
21
+
22
+ ```bash
23
+ (cd backend && go test ./...) # auth policy + route tests
24
+ synopkgland build-spk # → dist/__PACKAGE__-0.1.0-0001-{x86_64,aarch64}.spk
25
+ synopkgland inspect --spk dist/__PACKAGE__-0.1.0-0001-x86_64.spk
26
+ ```
27
+
28
+ Install: Package Center → Settings → General → Trust Level "Any publisher",
29
+ then Manual Install with the SPK for your NAS's CPU. Open
30
+ **__DISPLAY_NAME__** from the DSM main menu: it shows who you are signed in
31
+ as, what the backend reports, and (for administrators) a form that stores a
32
+ greeting in the package's data folder.
33
+
34
+ ## Project layout
35
+
36
+ | Path | What it is | Edit it? |
37
+ | --- | --- | --- |
38
+ | `dsm-package.yaml` | Package identity, version/build, arches, where backend and UI come from | Yes |
39
+ | `ui/app.js`, `ui/app.css` | The app inside the DSM window | Yes — your UI |
40
+ | `ui/…` | Any other assets; served from `/webman/3rdparty/__PACKAGE__/app/` | Yes |
41
+ | `i18n/messages/<locale>.json` | UI text in nine languages | Yes |
42
+ | `i18n/glossary.json` | Synology's official DSM terms, and which messages must use them | Add terms |
43
+ | `backend/internal/app/` | Your API: routes and handlers | Yes — your backend |
44
+ | `backend/internal/api/` | The CGI front door: guards, auth order, route policies | Rarely |
45
+ | `backend/internal/dsm/` | DSM session check and administrators lookup | Rarely |
46
+ | `backend/cmd/app/` | Wires the above together | Rarely |
47
+ | `dist/` | Built SPKs (ignored by git) | No |
48
+
49
+ `build-spk` generates everything else: `INFO`, `conf/privilege`
50
+ (`run-as: package`), no-op lifecycle scripts, icons, the DSM window wrapper
51
+ (`ui/config`, `ui/window-<build>.js`) and the CGI at `ui/cgi/api.cgi`.
52
+
53
+ ## Add an API operation
54
+
55
+ Add a route in `backend/internal/app/routes.go`. The policy is part of the
56
+ route, so you cannot register a handler without one:
57
+
58
+ ```go
59
+ func Routes(cfg Config) []api.Route {
60
+ return []api.Route{
61
+ api.User("app.status", cfg.status),
62
+ api.User("greeting.get", cfg.getGreeting),
63
+ api.Admin("greeting.set", cfg.setGreeting),
64
+ api.User("notes.list", cfg.listNotes), // new: any signed-in user
65
+ api.Admin("notes.purge", cfg.purgeNotes), // new: administrators only
66
+ }
67
+ }
68
+
69
+ func (cfg Config) listNotes(ctx context.Context, caller api.Caller, params json.RawMessage) (any, error) {
70
+ var in struct{ Limit int `json:"limit"` }
71
+ if err := decodeParams(params, &in); err != nil {
72
+ return nil, err
73
+ }
74
+ return map[string]any{"owner": caller.User(), "notes": []string{}}, nil
75
+ }
76
+ ```
77
+
78
+ Then add the method's valid params to `sampleParams` in `routes_test.go`; the
79
+ test fails until you do, and once you do it proves the new route refuses
80
+ callers without a session (and non-admins, for `api.Admin`) without touching
81
+ the data folder.
82
+
83
+ Call it from the UI with `ctx.call("notes.list", { limit: 20 })`. Errors
84
+ reject with `{ code, message }`, for example `session_required`,
85
+ `admin_required`, `bad_request` or `internal`.
86
+
87
+ ### Who can call what
88
+
89
+ | Declared with | No session | Signed-in user | Administrator |
90
+ | --- | --- | --- | --- |
91
+ | `api.User` | `session_required` | runs | runs |
92
+ | `api.Admin` | `session_required` | `admin_required` | runs |
93
+ | `api.Public` | runs | runs | runs |
94
+ | not registered | `session_required` | `unknown_method` | `unknown_method` |
95
+
96
+ Every reply, error or not, is HTTP 200 with `{"success": false, "error":
97
+ {"code", "message"}}` on failure, the same convention as DSM's own WebAPI.
98
+ DSM's web server replaces the body of any CGI reply with status 403, 404 or
99
+ 5xx by its own HTML error page, so a status-coded error would never reach your
100
+ UI.
101
+
102
+ - Handlers get an `api.Caller` the server built after authenticating; code
103
+ outside `internal/api` cannot construct an authenticated one.
104
+ - `api.Public` runs with **no** DSM login. Use it only for data you would show
105
+ anyone who can reach DSM's port; the template ships none.
106
+ - "Administrator" means a member of DSM's `administrators` group, read fresh
107
+ from `/etc/group` on every request.
108
+
109
+ ## How authentication works (and its limits)
110
+
111
+ The CGI hands only the session carriers (`Cookie`, `X-SYNO-TOKEN`, client
112
+ address) to DSM's own `/usr/syno/synoman/webman/modules/authenticate.cgi`,
113
+ which prints the signed-in user name. The backend never parses the session
114
+ cookie and never takes a user name or admin flag from the request.
115
+
116
+ Cross-site requests are refused: the API accepts only `POST` with
117
+ `Content-Type: application/json` (which forces a CORS preflight DSM never
118
+ approves), and it rejects `Sec-Fetch-Site` other than `same-origin` and any
119
+ foreign `Origin`. The DSM window wrapper sends DSM's `SynoToken`, but when DSM
120
+ has `enable_syno_token=no` that token proves nothing, so the same-origin
121
+ checks are what stop cross-site requests.
122
+
123
+ Known limits:
124
+
125
+ - Each request reuses the browser's DSM session; it does not carry DSM's
126
+ per-request `X-SYNO-HASH` proof. Verifying that proof needs private,
127
+ version-specific DSM libraries through a C bridge, so it is not part of this
128
+ template.
129
+ - Users from a directory service (LDAP/AD) who are administrators only
130
+ through directory groups are not listed in `/etc/group`, so they count as
131
+ ordinary users. This errs towards refusal.
132
+
133
+ ## Customize the UI
134
+
135
+ `ui/app.js` must register `window.DsmPackageApps[<package>].mount(root, ctx)`
136
+ (the template shows how, using `document.currentScript.dataset.package`).
137
+ `ctx.call(method, params)` is the only transport. `ctx.reopen(fn)` runs `fn`
138
+ whenever DSM shows the window again. Scope your CSS under your own root class:
139
+ every DSM app shares one page.
140
+
141
+ To build the UI with Vite, Vue or React, point `ui` at the build output and
142
+ give the build command as an argv list (no shell):
143
+
144
+ ```yaml
145
+ ui:
146
+ dir: web/dist
147
+ build:
148
+ cwd: web
149
+ command: [npm, run, build]
150
+ ```
151
+
152
+ The build must emit `app.js` (one script that registers `mount`) and
153
+ `app.css`. The public `@synopackageland/ui@0.4.0` package on
154
+ `https://registry.npmjs.org` gives Vue components with DSM 7 styling;
155
+ import its `themes/dsm7-embedded.css` for use inside a DSM window.
156
+
157
+ ## Languages
158
+
159
+ The UI ships in nine of DSM's languages: `en`, `zh-TW`,
160
+ `zh-CN`, `ja`, `ko`, `de`, `fr`, `es` and `it`. DSM's other languages show
161
+ English. The window follows DSM's own language setting (`_S("lang")`).
162
+
163
+ - **Messages**: `i18n/messages/<locale>.json`, flat `"key": "text"`. Use
164
+ `ctx.t("greeting.setBy", { text, user })` in the UI; `{name}` is a
165
+ placeholder, and `ctx.t("files", { count: 3 })` picks `files.one` or
166
+ `files.other`. Missing keys fall back to English, then to the key itself.
167
+ `build-spk` embeds the catalogs in the DSM window script, so there is no
168
+ extra request.
169
+ - **Errors**: the backend returns a `code`; the UI shows
170
+ `error.<code>` from the catalogs and uses the backend's English text only for
171
+ codes it has no message for. Add `error.<code>` for every code you return.
172
+ - **Glossary**: `i18n/glossary.json` records DSM's own words in all nine
173
+ languages, each with its source (`section/key` in DSM 7.4's
174
+ `/usr/syno/synoman/webman/texts/<lang>/strings`). A term's `keys` lists the
175
+ messages that must contain it; that is how "Save" uses DSM's own word in
176
+ every language, the same as the rest of DSM. Product names in `untranslated`
177
+ stay in English everywhere.
178
+ - **Package Center text**: `i18n.info` in `dsm-package.yaml` writes INFO
179
+ `displayname_<lang>` / `description_<lang>` (for example `description_cht`).
180
+
181
+ `synopkgland validate` and `build-spk` refuse a language file with missing or
182
+ extra keys, a translation that drops a `{placeholder}`, an unsupported
183
+ language, or a listed message that drifts from the glossary.
184
+
185
+ To add a message: add the key to `en.json` and to the other eight files. To
186
+ use a DSM term, copy its `text` from the glossary and add your key to the
187
+ term's `keys`. A Vue or React UI can keep its own i18n library; `ctx.t` and
188
+ `ctx.locale` are there if you want them.
189
+
190
+ ## Package an existing binary
191
+
192
+ Keep the Go backend and call your tool from a handler (recommended: auth
193
+ stays central), or replace the CGI with your own executable:
194
+
195
+ ```yaml
196
+ arches: [x86_64]
197
+ backend:
198
+ binary:
199
+ x86_64: prebuilt/api-x86_64
200
+ ```
201
+
202
+ The file must be a regular file in the project, a 64-bit Linux ELF for that
203
+ arch, and statically linked (DSM models ship different libc versions). It is
204
+ installed as `ui/cgi/api.cgi` (mode 0755) and runs as the package account.
205
+ It then owns everything `backend/internal/api` does for you: bounded reads,
206
+ POST-only JSON, same-origin checks, authentication before any work, and a
207
+ refusal for unknown methods.
208
+
209
+ ## Data, upgrades, uninstall
210
+
211
+ The backend stores data in `/var/packages/__PACKAGE__/var` (the package's
212
+ own folder, kept across upgrades). Bump `build` for every SPK you hand out;
213
+ bump `version` for feature releases. There are no lifecycle scripts to write:
214
+ the CGI starts per request, so start/stop do nothing.
215
+
216
+ ## Root access
217
+
218
+ By default (`privilege: none`) nothing in this package ever runs as root, and
219
+ that is the right choice for almost every app. Lifecycle scripts and the CGI
220
+ run as the package account, and nothing the package account can write is ever
221
+ run as root.
222
+
223
+ If your app truly needs root for a few fixed actions, create the project with
224
+ `synopkgland init -t native-go --privilege root`. That sets `privilege: root`
225
+ and adds:
226
+
227
+ - `root/whoami.sh`: one root operation. Each `root/<name>.sh` (lowercase name,
228
+ `#!/bin/sh`, at most 16 KiB) is one operation. The demo only reports the
229
+ user ID it runs as (`{"euid":0}`). Replace it with your own fixed actions.
230
+ - `backend/internal/app/root.go`: the `root.output` route (administrators
231
+ only) that returns an operation's last result.
232
+
233
+ How it works, with no keys or signing involved:
234
+
235
+ 1. `build-spk` embeds each operation's exact task text in the DSM window. The
236
+ SPK payload carries no root script, and the package still runs as its own
237
+ account.
238
+ 2. The window shows an authorization panel above your app, in the user's
239
+ DSM language. A DSM administrator clicks **Authorize** and confirms with
240
+ their DSM password in DSM's own dialog. DSM then creates one **disabled**
241
+ root task in Task Scheduler, named `<package> root: <name>`. The
242
+ confirmation is used for that one task and never stored.
243
+ 3. **Run** checks that the task is still exactly the approved text, runs it
244
+ with the administrator's session and shows its result. The task writes
245
+ only to `/run/synopkgland-<package>/<name>.out`, which root owns and your
246
+ backend can only read.
247
+ 4. The task text includes the package version, so after an upgrade the panel
248
+ shows "needs authorization again" and the old task never runs. A
249
+ same-named task that the panel did not create is never run, replaced or
250
+ deleted.
251
+ 5. **Remove** deletes the task, also an outdated one, without asking for the
252
+ password. Remove it before uninstalling the package, because DSM keeps
253
+ Task Scheduler tasks after an uninstall.
254
+
255
+ Non-administrators see DSM's "Only administrator accounts can perform this
256
+ operation." and nothing else. HTTP requests can never choose what runs as
257
+ root: the only root code is the fixed text an administrator approved. Your app
258
+ can also use `ctx.root` (`state`, `authorize`, `run`, `remove`, `output`) to
259
+ drive the same flow from its own UI.
260
+
261
+ ## Support matrix
262
+
263
+ | | Status |
264
+ | --- | --- |
265
+ | Build hosts | macOS, Linux (any OS with Go and Node) |
266
+ | Target CPUs | x86_64 and aarch64 DSM models (one SPK each) |
267
+ | DSM | 7.2 or later declared (`os_min_ver`). Installed and tested on DSM 7.4 (x86_64). aarch64 SPKs are built the same way but have not been run on a NAS. |
268
+ | Not supported | 32-bit ARM, DSM 6, running anything as root without an administrator's approval |
@@ -0,0 +1,49 @@
1
+ // Command app is the package backend. DSM runs it as a CGI (ui/cgi/api.cgi in
2
+ // the package) under the package account, once per request.
3
+ package main
4
+
5
+ import (
6
+ "encoding/json"
7
+ "fmt"
8
+ "log"
9
+ "net/http/cgi"
10
+ "os"
11
+
12
+ "dsmapp/internal/api"
13
+ "dsmapp/internal/app"
14
+ "dsmapp/internal/dsm"
15
+ "dsmapp/internal/helper"
16
+ )
17
+
18
+ // Set by `synopkgland build-spk` through -ldflags -X; never read from a request.
19
+ var (
20
+ packageID = "dev"
21
+ version = "0.0.0"
22
+ helperInventory = ""
23
+ )
24
+
25
+ func main() {
26
+ log.SetFlags(0)
27
+ log.SetPrefix(packageID + ": ")
28
+ if os.Getenv("GATEWAY_INTERFACE") == "" {
29
+ fmt.Fprintf(os.Stderr, "%s %s: this program runs as the package's DSM CGI; open the app from the DSM main menu.\n", packageID, version)
30
+ os.Exit(2)
31
+ }
32
+ cfg := app.Config{
33
+ PackageID: packageID,
34
+ Version: version,
35
+ DataDir: "/var/packages/" + packageID + "/var",
36
+ }
37
+ if helperInventory != "" {
38
+ if json.Unmarshal([]byte(helperInventory), &cfg.HelperInventory) != nil || cfg.HelperInventory.Package != packageID || cfg.HelperInventory.Version != version {
39
+ cfg.HelperInventory = helper.Inventory{Package: packageID}
40
+ }
41
+ }
42
+ srv, err := api.NewServer(dsm.NewAuthenticator(), app.Routes(cfg)...)
43
+ if err != nil {
44
+ log.Fatal(err)
45
+ }
46
+ if err := cgi.Serve(srv); err != nil {
47
+ log.Fatal(err)
48
+ }
49
+ }
@@ -0,0 +1,38 @@
1
+ // Built only when dsm-package.yaml explicitly opts into integration.
2
+ package main
3
+
4
+ import (
5
+ "context"
6
+ "dsmapp/internal/helper"
7
+ "encoding/json"
8
+ "fmt"
9
+ "os"
10
+ )
11
+
12
+ var helperInventory string
13
+
14
+ func main() {
15
+ var i helper.Inventory
16
+ if len(os.Args) != 2 || json.Unmarshal([]byte(helperInventory), &i) != nil {
17
+ os.Exit(2)
18
+ }
19
+ if os.Args[1] == "check-stopped" || os.Args[1] == "diagnose" {
20
+ c, err := helper.LocalClient(i)
21
+ if err != nil {
22
+ os.Exit(1)
23
+ }
24
+ if os.Args[1] == "diagnose" {
25
+ json.NewEncoder(os.Stdout).Encode(c.Status(context.Background()))
26
+ return
27
+ }
28
+ if c.CheckStopped(context.Background()) != nil {
29
+ os.Exit(1)
30
+ }
31
+ return
32
+ }
33
+ if err := helper.RunLinux(i, os.Args[1]); err != nil {
34
+ fmt.Fprintln(os.Stderr, "helper lifecycle refused")
35
+ os.Exit(1)
36
+ }
37
+ fmt.Println(`{"success":true}`)
38
+ }
@@ -0,0 +1,3 @@
1
+ module dsmapp
2
+
3
+ go 1.24
@@ -0,0 +1,33 @@
1
+ package api
2
+
3
+ import (
4
+ "errors"
5
+ "log"
6
+ )
7
+
8
+ // Error is an API error the browser may see. Any other error a handler
9
+ // returns is logged and reported only as "internal". Errors travel in the
10
+ // JSON envelope with HTTP 200, as DSM's own WebAPI does: DSM's web server
11
+ // replaces the body of a CGI's 403, 404 and 5xx replies with its own page.
12
+ type Error struct {
13
+ Code string
14
+ Message string
15
+ }
16
+
17
+ func (e *Error) Error() string { return e.Code + ": " + e.Message }
18
+
19
+ // BadRequest reports invalid params.
20
+ func BadRequest(message string) *Error {
21
+ return &Error{"bad_request", message}
22
+ }
23
+
24
+ var internalError = &Error{"internal", "internal error"}
25
+
26
+ func asError(err error) *Error {
27
+ var apiErr *Error
28
+ if errors.As(err, &apiErr) {
29
+ return apiErr
30
+ }
31
+ log.Printf("handler error: %v", err)
32
+ return internalError
33
+ }
@@ -0,0 +1,94 @@
1
+ // Package api is the package's one HTTP entry point. Every request passes the
2
+ // same transport guards and, unless its route was explicitly declared Public,
3
+ // one DSM authentication before any handler runs. The policy is part of the
4
+ // route itself: a Route can only be made through User, Admin or Public, so a
5
+ // handler cannot be registered without saying who may call it.
6
+ package api
7
+
8
+ import (
9
+ "context"
10
+ "encoding/json"
11
+ "errors"
12
+ "fmt"
13
+ "regexp"
14
+ )
15
+
16
+ type policy int
17
+
18
+ const (
19
+ policyUnset policy = iota
20
+ policyUser
21
+ policyAdmin
22
+ policyPublic
23
+ )
24
+
25
+ // Handler runs one business operation. It receives the caller the server
26
+ // verified and the raw `params` object; it decodes params itself.
27
+ type Handler func(ctx context.Context, caller Caller, params json.RawMessage) (any, error)
28
+
29
+ // Route binds a method name to its handler and access policy. Its fields are
30
+ // private: build routes with User, Admin or Public.
31
+ type Route struct {
32
+ method string
33
+ policy policy
34
+ handler Handler
35
+ }
36
+
37
+ // User declares an operation any signed-in DSM user may call.
38
+ func User(method string, h Handler) Route { return Route{method, policyUser, h} }
39
+
40
+ // Admin declares an operation only members of DSM's administrators group may call.
41
+ func Admin(method string, h Handler) Route { return Route{method, policyAdmin, h} }
42
+
43
+ // Public declares an operation that runs WITHOUT a DSM session. Use it only for
44
+ // data that is safe to show anyone who can reach the NAS's DSM port.
45
+ func Public(method string, h Handler) Route { return Route{method, policyPublic, h} }
46
+
47
+ // Identity is what an Authenticator established about the requester.
48
+ type Identity struct {
49
+ User string
50
+ Admin bool
51
+ }
52
+
53
+ // Caller is the verified requester handed to handlers. Only the server sets
54
+ // its fields, so a handler cannot fabricate an authenticated caller; the zero
55
+ // value carries no authority.
56
+ type Caller struct {
57
+ identity Identity
58
+ authenticated bool
59
+ }
60
+
61
+ func (c Caller) Authenticated() bool { return c.authenticated }
62
+ func (c Caller) User() string { return c.identity.User }
63
+ func (c Caller) Admin() bool { return c.authenticated && c.identity.Admin }
64
+
65
+ var methodName = regexp.MustCompile(`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$`)
66
+
67
+ func buildRegistry(routes []Route) (map[string]Route, error) {
68
+ registry := make(map[string]Route, len(routes))
69
+ for _, r := range routes {
70
+ if r.policy == policyUnset {
71
+ return nil, errors.New("route without access policy: build it with api.User, api.Admin or api.Public")
72
+ }
73
+ if !methodName.MatchString(r.method) {
74
+ return nil, fmt.Errorf("route %q: method must look like \"area.action\" (lowercase)", r.method)
75
+ }
76
+ if r.handler == nil {
77
+ return nil, fmt.Errorf("route %q has no handler", r.method)
78
+ }
79
+ if _, dup := registry[r.method]; dup {
80
+ return nil, fmt.Errorf("route %q is registered twice", r.method)
81
+ }
82
+ registry[r.method] = r
83
+ }
84
+ return registry, nil
85
+ }
86
+
87
+ // Method is the route's method name, e.g. "greeting.set".
88
+ func (r Route) Method() string { return r.method }
89
+
90
+ // Public reports whether the route was declared with Public.
91
+ func (r Route) Public() bool { return r.policy == policyPublic }
92
+
93
+ // AdminOnly reports whether the route was declared with Admin.
94
+ func (r Route) AdminOnly() bool { return r.policy == policyAdmin }