@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
@@ -1,15 +1,11 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { IDENTITY_CGI_PATH } from "./broker-paths.js";
6
- import { packageSourceOrigin } from "../deploy/catalog-publish.js";
7
2
  import { listShareNamesWithSynoagentcli, lookupSharePathWithSynoagentcli } from "./share-lookup.js";
8
3
  import { createSynoagentcliApiCall, fetchSharePermissions, fetchUserGroups, } from "./share-permission-lookup.js";
4
+ import { fetchPortInfo } from "./port-info-lookup.js";
9
5
  import { dsmFetch } from "./dsm-http.js";
10
- import { installPackageFromCatalog, installPackageFromLocalSpk, uninstallPackageFromDsm, fetchInstalledPackageState, packageStateFromRecord, } from "./catalog-install.js";
6
+ import { fetchDsmPortProfileInventory } from "./dsm-port-profile.js";
7
+ import { installPackageFromLocalSpk, fetchInstalledPackageState, packageStateFromRecord, } from "./catalog-install.js";
11
8
  import { resolveHostCredentials } from "./credentials.js";
12
- const OIDC_COMPAT_PATH = "/api/v1/inner-loop/oidc-compat";
13
9
  export function createNasHost(options = {}) {
14
10
  const credentials = options.credentials ?? resolveHostCredentials(options.host);
15
11
  if (!credentials) {
@@ -59,9 +55,7 @@ export function homesPathFromDsmUserHomeRead(result) {
59
55
  }
60
56
  function createDsmNasHost(credentials, profileName) {
61
57
  const dsmOrigin = credentials.dsmOrigin.replace(/\/$/, "");
62
- const brokerBaseUrl = `${dsmOrigin}${IDENTITY_CGI_PATH}`;
63
58
  let cookieHeader = credentials.cookieHeader;
64
- const projectEnvCache = new Map();
65
59
  const ensureLogin = async () => {
66
60
  if (cookieHeader) {
67
61
  return;
@@ -75,7 +69,10 @@ function createDsmNasHost(credentials, profileName) {
75
69
  session: "FileStation",
76
70
  format: "cookie",
77
71
  });
78
- const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi?${query.toString()}`);
72
+ const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: query });
73
+ const login = await response.json();
74
+ if (!response.ok || login.success !== true)
75
+ throw new Error("DSM login failed");
79
76
  const setCookie = typeof response.headers.getSetCookie === "function"
80
77
  ? response.headers.getSetCookie()
81
78
  : response.headers.get("set-cookie")
@@ -85,140 +82,18 @@ function createDsmNasHost(credentials, profileName) {
85
82
  .map((line) => line.split(";")[0]?.trim())
86
83
  .filter((pair) => Boolean(pair));
87
84
  cookieHeader = pairs.join("; ");
88
- };
89
- const ensureInnerLoopApi = async () => {
90
- await ensureLogin();
91
- const response = await dsmFetch(`${brokerBaseUrl}/api/v1/inner-loop/diagnostics?package=probe`, { headers: { cookie: cookieHeader ?? "" } });
92
- const text = await response.text();
93
- if (response.status === 404 || text.trimStart().startsWith("<")) {
94
- const error = new Error("Broker inner-loop API 不可用(收到 HTML 404)。請將 SynoPkgLandService 升級到 >=0.1.0-0016。");
95
- error.code = "INNER_LOOP_API_MISSING";
96
- throw error;
97
- }
98
- try {
99
- JSON.parse(text);
100
- }
101
- catch {
102
- const error = new Error("Broker inner-loop API 回傳非 JSON 內容。");
103
- error.code = "INNER_LOOP_API_MISSING";
104
- throw error;
105
- }
106
- };
107
- const ensurePackageSourceFeed = (origin) => {
108
- const feed = origin.replace(/\/$/, "") + "/";
109
- const list = synoagentcli(["dsm", "package-center", "source", "list"], profileName);
110
- if (list.status !== 0) {
111
- return;
112
- }
113
- const parsed = JSON.parse(list.stdout);
114
- const items = parsed.data?.items ?? [];
115
- const normalizedFeed = feed.replace(/\/$/, "");
116
- const existing = items.find((item) => item.feed?.replace(/\/$/, "") === normalizedFeed);
117
- if (existing) {
118
- return;
119
- }
120
- const stale = items.find((item) => item.name === "package-land-throwaway");
121
- const payload = { name: "package-land-throwaway", feed };
122
- const staging = mkdtempSync(join(tmpdir(), "syno-package-source-feed-"));
123
- const jsonPath = join(staging, "source.json");
124
- writeFileSync(jsonPath, JSON.stringify(payload));
125
- try {
126
- if (stale?.feed) {
127
- synoagentcli(["dsm", "package-center", "source", "update", stale.feed, "--json-file", jsonPath], profileName);
128
- return;
129
- }
130
- synoagentcli(["dsm", "package-center", "source", "add", "--json-file", jsonPath], profileName);
131
- }
132
- finally {
133
- rmSync(staging, { recursive: true, force: true });
134
- }
85
+ if (!cookieHeader)
86
+ throw new Error("DSM login returned no session cookie");
135
87
  };
136
88
  return {
137
89
  dsmOrigin,
138
- installationId: "live-nas",
139
- async requestAttestation(input) {
140
- await ensureInnerLoopApi();
90
+ async installFromLocalSpk(input) {
141
91
  await ensureLogin();
142
- const response = await dsmFetch(`${brokerBaseUrl}/api/v1/inner-loop/attest`, {
143
- method: "POST",
144
- headers: {
145
- cookie: cookieHeader ?? "",
146
- "content-type": "application/json",
147
- },
148
- body: JSON.stringify(input),
92
+ await installPackageFromLocalSpk({
93
+ dsmOrigin, cookieHeader: cookieHeader ?? "",
94
+ packageId: input.packageId, spkPath: input.spkPath,
95
+ version: input.version, profileName, extraValues: input.extraValues,
149
96
  });
150
- const text = await response.text();
151
- let body;
152
- try {
153
- body = JSON.parse(text);
154
- }
155
- catch {
156
- const error = new Error("Broker inner-loop attest 回傳非 JSON 內容。");
157
- error.code = "INNER_LOOP_API_MISSING";
158
- throw error;
159
- }
160
- if (!response.ok) {
161
- const error = new Error(body.error?.message ?? `Attestation failed (${response.status})`);
162
- error.code = body.error?.code;
163
- throw error;
164
- }
165
- const token = typeof body.token === "string" && body.token.length > 0 ? body.token : undefined;
166
- return {
167
- kind: "local",
168
- attestationDigest: body.attestationDigest ?? "",
169
- ...(token !== undefined ? { token } : {}),
170
- };
171
- },
172
- async ensurePackageSource(origin) {
173
- ensurePackageSourceFeed(origin);
174
- },
175
- async installFromCatalog(input) {
176
- try {
177
- ensurePackageSourceFeed(packageSourceOrigin());
178
- await ensureLogin();
179
- await installPackageFromCatalog({
180
- dsmOrigin,
181
- cookieHeader: cookieHeader ?? "",
182
- packageId: input.packageId,
183
- profileName,
184
- expectedVersion: input.version,
185
- packageSourceOrigin: packageSourceOrigin(),
186
- extraValues: input.extraValues,
187
- });
188
- }
189
- finally {
190
- projectEnvCache.delete(input.packageId);
191
- }
192
- },
193
- async installFromLocalSpk(input) {
194
- try {
195
- await ensureLogin();
196
- await installPackageFromLocalSpk({
197
- dsmOrigin,
198
- cookieHeader: cookieHeader ?? "",
199
- packageId: input.packageId,
200
- spkPath: input.spkPath,
201
- version: input.version,
202
- profileName,
203
- extraValues: input.extraValues,
204
- });
205
- }
206
- finally {
207
- projectEnvCache.delete(input.packageId);
208
- }
209
- },
210
- async uninstallPackage(packageIdentity) {
211
- try {
212
- await ensureLogin();
213
- await uninstallPackageFromDsm({
214
- dsmOrigin,
215
- cookieHeader: cookieHeader ?? "",
216
- packageId: packageIdentity,
217
- });
218
- }
219
- finally {
220
- projectEnvCache.delete(packageIdentity);
221
- }
222
97
  },
223
98
  async getArchFamily() {
224
99
  await ensureLogin();
@@ -241,51 +116,22 @@ function createDsmNasHost(credentials, profileName) {
241
116
  async listUserGroups(userName) {
242
117
  return fetchUserGroups(userName, createSynoagentcliApiCall(profileName));
243
118
  },
244
- async resolveHomesPath() {
119
+ async listPortInfo() {
245
120
  try {
246
- const result = synoagentcli(["dsm", "control-panel", "user-group", "advanced", "get", "--section", "home"], profileName);
247
- return homesPathFromDsmUserHomeRead(result);
121
+ await ensureLogin();
122
+ return fetchPortInfo(dsmOrigin, cookieHeader ?? "");
248
123
  }
249
124
  catch {
250
125
  return undefined;
251
126
  }
252
127
  },
253
- async getInstalledProjectEnv(packageIdentity) {
254
- const cached = projectEnvCache.get(packageIdentity);
255
- if (cached) {
256
- return cached;
257
- }
258
- const lookup = (async () => {
259
- await ensureLogin();
260
- const response = await dsmFetch(`${brokerBaseUrl}/api/v1/inner-loop/project-env/${encodeURIComponent(packageIdentity)}`, { headers: { cookie: cookieHeader ?? "" } });
261
- if (response.ok) {
262
- const payload = (await response.json());
263
- if (!payload.env || typeof payload.env !== "object" || Array.isArray(payload.env)) {
264
- return undefined;
265
- }
266
- return Object.fromEntries(Object.entries(payload.env).filter((entry) => typeof entry[1] === "string"));
267
- }
268
- return undefined;
269
- })();
270
- projectEnvCache.set(packageIdentity, lookup);
271
- return lookup;
272
- },
273
- async getAttestationInfo(packageIdentity) {
274
- await ensureLogin();
275
- const response = await dsmFetch(`${brokerBaseUrl}/api/v1/inner-loop/info/${encodeURIComponent(packageIdentity)}`, { headers: { cookie: cookieHeader ?? "" } });
276
- const text = await response.text();
277
- if (response.status === 404 || text.trimStart().startsWith("<")) {
278
- const error = new Error("Broker inner-loop info 不可用(HTML 404)。");
279
- error.code = "INNER_LOOP_API_MISSING";
280
- throw error;
281
- }
128
+ async resolveHomesPath() {
282
129
  try {
283
- return JSON.parse(text);
130
+ const result = synoagentcli(["dsm", "control-panel", "user-group", "advanced", "get", "--section", "home"], profileName);
131
+ return homesPathFromDsmUserHomeRead(result);
284
132
  }
285
133
  catch {
286
- const error = new Error("Broker inner-loop info 回傳非 JSON 內容。");
287
- error.code = "INNER_LOOP_API_MISSING";
288
- throw error;
134
+ return undefined;
289
135
  }
290
136
  },
291
137
  async getInstalledPackageState(packageIdentity) {
@@ -344,101 +190,15 @@ function createDsmNasHost(credentials, profileName) {
344
190
  .filter(Boolean)
345
191
  .map((line) => `[status] ${line}`);
346
192
  },
347
- async getDsmDiagnostics(packageIdentity) {
348
- try {
349
- await ensureLogin();
350
- const pkg = packageIdentity ?? "probe";
351
- const response = await dsmFetch(`${brokerBaseUrl}/api/v1/inner-loop/diagnostics?package=${encodeURIComponent(pkg)}`, { headers: { cookie: cookieHeader ?? "" } });
352
- if (!response.ok) {
353
- return undefined;
354
- }
355
- const text = await response.text();
356
- return JSON.parse(text);
357
- }
358
- catch {
359
- return undefined;
360
- }
361
- },
362
- async getOidcCompatSettings() {
193
+ async getDsmPortProfileInventory() {
363
194
  try {
364
195
  await ensureLogin();
365
- const response = await dsmFetch(`${brokerBaseUrl}${OIDC_COMPAT_PATH}`, {
366
- headers: { cookie: cookieHeader ?? "" },
367
- });
368
- const text = await response.text();
369
- if (response.status === 404 || text.trimStart().startsWith("<")) {
370
- return undefined;
371
- }
372
- if (!response.ok) {
373
- return undefined;
374
- }
375
- const payload = JSON.parse(text);
376
- const settings = payload.settings;
377
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
378
- return undefined;
379
- }
380
- const record = settings;
381
- const origin = record.origin;
382
- if (!origin || typeof origin !== "object" || Array.isArray(origin)) {
383
- return undefined;
384
- }
385
- const originRecord = origin;
386
- if (typeof record.enabled !== "boolean" ||
387
- typeof originRecord.ready !== "boolean" ||
388
- typeof originRecord.kind !== "string" ||
389
- typeof originRecord.setupHint !== "string") {
390
- return undefined;
391
- }
392
- return {
393
- enabled: record.enabled,
394
- origin: {
395
- ready: originRecord.ready,
396
- kind: originRecord.kind,
397
- hostname: typeof originRecord.hostname === "string" ? originRecord.hostname : undefined,
398
- issuerUrl: typeof originRecord.issuerUrl === "string" ? originRecord.issuerUrl : undefined,
399
- setupHint: originRecord.setupHint,
400
- },
401
- };
196
+ return await fetchDsmPortProfileInventory(dsmOrigin, cookieHeader ?? "");
402
197
  }
403
198
  catch {
404
199
  return undefined;
405
200
  }
406
201
  },
407
- async smokeRequireLogin(input) {
408
- await ensureLogin();
409
- const info = await this.getAttestationInfo(input.packageIdentity);
410
- let openUrl = info.openUrl;
411
- if (!openUrl) {
412
- const pkg = synoagentcli(["dsm", "package-center", "get", input.packageIdentity], profileName);
413
- if (pkg.status === 0) {
414
- try {
415
- const parsed = JSON.parse(pkg.stdout);
416
- openUrl = parsed.data?.url?.[0];
417
- }
418
- catch {
419
- // ignore parse errors
420
- }
421
- }
422
- }
423
- if (!openUrl) {
424
- return { ok: false };
425
- }
426
- const origin = new URL(openUrl).origin;
427
- const response = await dsmFetch(`${brokerBaseUrl}/api/v1/auth/login`, {
428
- method: "POST",
429
- headers: {
430
- cookie: cookieHeader ?? input.cookieHeader,
431
- origin,
432
- "content-type": "application/json",
433
- },
434
- body: JSON.stringify({ packageId: input.packageIdentity }),
435
- });
436
- if (!response.ok) {
437
- return { ok: false };
438
- }
439
- const body = (await response.json());
440
- return { ok: Boolean(body.user?.username), username: body.user?.username };
441
- },
442
202
  };
443
203
  }
444
204
  /**
@@ -0,0 +1,47 @@
1
+ /** The precision caveat for the administrator-only fallback. */
2
+ export declare const DSM_PORT_PROFILE_PRECISION_WARNING = "API \u7248\u672A\u8B80\u53D6 conf/resource\uFF1B\u82E5\u6709 App \u81EA\u8A02 Compose project \u540D\u53EF\u80FD\u8AA4\u5831";
3
+ export interface DsmPortRange {
4
+ start: number;
5
+ end: number;
6
+ }
7
+ export interface DsmPortProfile {
8
+ portId: string;
9
+ containerName: string;
10
+ projectName: string;
11
+ /** Kept for diagnostics only; the API orphan test must use projectName. */
12
+ presumedPackageIdentity: string;
13
+ dstPorts: DsmPortRange[];
14
+ }
15
+ export interface DsmPortProfileInventory {
16
+ profiles: DsmPortProfile[];
17
+ orphaned: DsmPortProfile[];
18
+ }
19
+ /**
20
+ * parsePortProfileFilename(). In particular, projectName is everything before
21
+ * the final two dash-separated container segments.
22
+ */
23
+ export declare function parsePortProfileFilename(filename: string): {
24
+ containerName: string;
25
+ projectName: string;
26
+ presumedPackageIdentity: string;
27
+ } | undefined;
28
+ /** Parse one DSM dst_port value, including the documented range form. */
29
+ export declare function parseDsmPortRange(value: string): DsmPortRange | undefined;
30
+ /** Parse a successful SYNO.Core.Service.PortInfo/load response. */
31
+ export declare function parseDsmPortInfoPayload(payload: unknown): DsmPortProfile[] | undefined;
32
+ /** Parse the installed identity set from a successful DSM package list. */
33
+ export declare function installedPackageIdentitiesFromPayload(payload: unknown): string[] | undefined;
34
+ /**
35
+ * Build the API-only inventory from the two independent DSM responses.
36
+ *
37
+ * The package list is a forward mapping: package identity -> project name.
38
+ * Do not use presumedPackageIdentity here; replacing dashes with dots is
39
+ * lossy for identities such as com.example.hello-web.
40
+ */
41
+ export declare function dsmPortProfileInventoryFromPayloads(portInfoPayload: unknown, packageListPayload: unknown): DsmPortProfileInventory | undefined;
42
+ /**
43
+ * Read PortInfo.load and the administrator-visible installed package list.
44
+ * Any transport, HTTP, JSON, or DSM API failure becomes undefined so callers
45
+ * can skip without inventing residue.
46
+ */
47
+ export declare function fetchDsmPortProfileInventory(dsmOrigin: string, cookieHeader: string): Promise<DsmPortProfileInventory | undefined>;
@@ -0,0 +1,206 @@
1
+ import { basename } from "node:path";
2
+ import { composeProjectName } from "../build-spk/resource-worker.js";
3
+ import { dsmFetch } from "./dsm-http.js";
4
+ /** The precision caveat for the administrator-only fallback. */
5
+ export const DSM_PORT_PROFILE_PRECISION_WARNING = "API 版未讀取 conf/resource;若有 App 自訂 Compose project 名可能誤報";
6
+ /**
7
+ * parsePortProfileFilename(). In particular, projectName is everything before
8
+ * the final two dash-separated container segments.
9
+ */
10
+ export function parsePortProfileFilename(filename) {
11
+ const base = basename(filename);
12
+ if (!base.startsWith("docker_") || !base.endsWith(".sc")) {
13
+ return undefined;
14
+ }
15
+ const containerName = base.slice("docker_".length, base.length - ".sc".length);
16
+ if (!containerName) {
17
+ return undefined;
18
+ }
19
+ // remove the final -<service>-<replica> pair, not an identity-specific guess.
20
+ const lastDash = containerName.lastIndexOf("-");
21
+ if (lastDash <= 0) {
22
+ return undefined;
23
+ }
24
+ const secondLastDash = containerName.lastIndexOf("-", lastDash - 1);
25
+ if (secondLastDash <= 0) {
26
+ return undefined;
27
+ }
28
+ const projectName = containerName.slice(0, secondLastDash);
29
+ const presumedPackageIdentity = projectName.replace(/-/g, ".");
30
+ return {
31
+ containerName,
32
+ projectName,
33
+ presumedPackageIdentity,
34
+ };
35
+ }
36
+ /** Parse one DSM dst_port value, including the documented range form. */
37
+ export function parseDsmPortRange(value) {
38
+ const match = /^([0-9]{1,5})(?:-([0-9]{1,5}))?$/.exec(value.trim());
39
+ if (!match) {
40
+ return undefined;
41
+ }
42
+ const start = Number(match[1]);
43
+ const end = Number(match[2] ?? match[1]);
44
+ if (!Number.isInteger(start) ||
45
+ !Number.isInteger(end) ||
46
+ start < 1 ||
47
+ end > 65535 ||
48
+ start > end) {
49
+ return undefined;
50
+ }
51
+ return { start, end };
52
+ }
53
+ function parseDsmPortRanges(value) {
54
+ if (!Array.isArray(value) || value.length === 0) {
55
+ return undefined;
56
+ }
57
+ const ranges = [];
58
+ for (const entry of value) {
59
+ if (typeof entry !== "string") {
60
+ return undefined;
61
+ }
62
+ const range = parseDsmPortRange(entry);
63
+ if (!range) {
64
+ return undefined;
65
+ }
66
+ ranges.push(range);
67
+ }
68
+ return ranges;
69
+ }
70
+ function record(value) {
71
+ return value && typeof value === "object" && !Array.isArray(value)
72
+ ? value
73
+ : undefined;
74
+ }
75
+ function nonEmptyString(value) {
76
+ return typeof value === "string" && value.length > 0 ? value : undefined;
77
+ }
78
+ function parseDockerPortInfo(value) {
79
+ const entry = record(value);
80
+ if (!entry) {
81
+ return undefined;
82
+ }
83
+ // PortInfo also contains ordinary DSM service rows. Only the documented
84
+ // Docker(TCP) rows correspond to docker_<container>.sc profiles.
85
+ if (entry.protocol !== "tcp" || entry.name !== "Docker(TCP)") {
86
+ return undefined;
87
+ }
88
+ const portId = nonEmptyString(entry.port_id);
89
+ if (!portId?.endsWith("_tcp")) {
90
+ return undefined;
91
+ }
92
+ const containerName = portId.slice(0, -"_tcp".length);
93
+ if (!containerName || entry.desc !== `Docker ${containerName}`) {
94
+ return undefined;
95
+ }
96
+ const parsed = parsePortProfileFilename(`docker_${containerName}.sc`);
97
+ const dstPorts = parseDsmPortRanges(entry.dst_port);
98
+ if (!parsed || !dstPorts) {
99
+ return undefined;
100
+ }
101
+ return {
102
+ portId,
103
+ containerName: parsed.containerName,
104
+ projectName: parsed.projectName,
105
+ presumedPackageIdentity: parsed.presumedPackageIdentity,
106
+ dstPorts,
107
+ };
108
+ }
109
+ /** Parse a successful SYNO.Core.Service.PortInfo/load response. */
110
+ export function parseDsmPortInfoPayload(payload) {
111
+ const root = record(payload);
112
+ const data = record(root?.data);
113
+ if (root?.success !== true || !data || !Array.isArray(data.port_info)) {
114
+ return undefined;
115
+ }
116
+ return data.port_info.flatMap((entry) => {
117
+ const profile = parseDockerPortInfo(entry);
118
+ return profile ? [profile] : [];
119
+ });
120
+ }
121
+ /** Parse the installed identity set from a successful DSM package list. */
122
+ export function installedPackageIdentitiesFromPayload(payload) {
123
+ const root = record(payload);
124
+ const data = record(root?.data);
125
+ if (root?.success !== true || !data || !Array.isArray(data.packages)) {
126
+ return undefined;
127
+ }
128
+ return [
129
+ ...new Set(data.packages
130
+ .map((entry) => record(entry)?.id)
131
+ .filter((id) => typeof id === "string" && id.length > 0)),
132
+ ];
133
+ }
134
+ /**
135
+ * Build the API-only inventory from the two independent DSM responses.
136
+ *
137
+ * The package list is a forward mapping: package identity -> project name.
138
+ * Do not use presumedPackageIdentity here; replacing dashes with dots is
139
+ * lossy for identities such as com.example.hello-web.
140
+ */
141
+ export function dsmPortProfileInventoryFromPayloads(portInfoPayload, packageListPayload) {
142
+ const profiles = parseDsmPortInfoPayload(portInfoPayload);
143
+ if (!profiles) {
144
+ return undefined;
145
+ }
146
+ // An empty PortInfo list is a safe, successful "nothing to report" result;
147
+ // there is no reason to turn an unavailable package list into an orphan.
148
+ if (profiles.length === 0) {
149
+ return { profiles, orphaned: [] };
150
+ }
151
+ const installedPackages = installedPackageIdentitiesFromPayload(packageListPayload);
152
+ // An empty or unavailable package list cannot prove that any profile is an
153
+ // orphan. Fail closed instead of classifying every visible Docker row.
154
+ if (!installedPackages || installedPackages.length === 0) {
155
+ return undefined;
156
+ }
157
+ const declaredProjectNames = new Set(installedPackages.map(composeProjectName));
158
+ const orphaned = profiles.filter((profile) => !declaredProjectNames.has(profile.projectName));
159
+ return { profiles, orphaned };
160
+ }
161
+ async function fetchJson(dsmOrigin, cookieHeader, params) {
162
+ const query = new URLSearchParams(params);
163
+ const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi?${query.toString()}`, {
164
+ headers: { cookie: cookieHeader },
165
+ });
166
+ if (!response.ok) {
167
+ return undefined;
168
+ }
169
+ try {
170
+ return await response.json();
171
+ }
172
+ catch {
173
+ return undefined;
174
+ }
175
+ }
176
+ /**
177
+ * Read PortInfo.load and the administrator-visible installed package list.
178
+ * Any transport, HTTP, JSON, or DSM API failure becomes undefined so callers
179
+ * can skip without inventing residue.
180
+ */
181
+ export async function fetchDsmPortProfileInventory(dsmOrigin, cookieHeader) {
182
+ try {
183
+ const portInfoPayload = await fetchJson(dsmOrigin, cookieHeader, {
184
+ api: "SYNO.Core.Service.PortInfo",
185
+ version: "1",
186
+ method: "load",
187
+ });
188
+ const profiles = parseDsmPortInfoPayload(portInfoPayload);
189
+ if (!profiles) {
190
+ return undefined;
191
+ }
192
+ if (profiles.length === 0) {
193
+ return { profiles, orphaned: [] };
194
+ }
195
+ const packageListPayload = await fetchJson(dsmOrigin, cookieHeader, {
196
+ api: "SYNO.Core.Package",
197
+ version: "2",
198
+ method: "list",
199
+ additional: JSON.stringify(["status"]),
200
+ });
201
+ return dsmPortProfileInventoryFromPayloads(portInfoPayload, packageListPayload);
202
+ }
203
+ catch {
204
+ return undefined;
205
+ }
206
+ }
@@ -0,0 +1,9 @@
1
+ import type { PortInfoEntry } from "./port-info.js";
2
+ export type PortInfoHttpRequest = (url: string, init?: RequestInit) => Promise<Response>;
3
+ /**
4
+ * Parses the native DSM response, not a synoagentcli operation envelope.
5
+ * `undefined` means "cannot tell" and deliberately allows deployment to
6
+ * continue. An empty `port_info` list has the same fail-open meaning.
7
+ */
8
+ export declare function parsePortInfo(payload: unknown): PortInfoEntry[] | undefined;
9
+ export declare function fetchPortInfo(dsmOrigin: string, cookieHeader: string, request?: PortInfoHttpRequest): Promise<PortInfoEntry[] | undefined>;