@synopackageland/cli 0.32.1 → 0.33.1

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 (75) hide show
  1. package/CHANGELOG.md +6 -6
  2. package/README.md +13 -3
  3. package/dist/cli.js +39 -11
  4. package/dist/ctl/cli.d.ts +1 -0
  5. package/dist/ctl/cli.js +73 -21
  6. package/dist/ctl/dsm-auth.d.ts +1 -10
  7. package/dist/ctl/dsm-auth.js +2 -30
  8. package/dist/ctl/errors.js +1 -1
  9. package/dist/ctl/input.js +3 -3
  10. package/dist/ctl/service.js +8 -8
  11. package/dist/ctl/store.d.ts +1 -51
  12. package/dist/ctl/store.js +2 -93
  13. package/dist/ctl/tls.d.ts +1 -0
  14. package/dist/ctl/tls.js +2 -0
  15. package/dist/ctl/transfers.d.ts +1 -14
  16. package/dist/ctl/transfers.js +2 -77
  17. package/dist/ctl/transport.d.ts +1 -12
  18. package/dist/ctl/transport.js +2 -83
  19. package/dist/e2e/dsm.d.ts +2 -2
  20. package/dist/e2e/dsm.js +33 -17
  21. package/dist/error-registry.js +10 -12
  22. package/dist/host/catalog-install.d.ts +0 -10
  23. package/dist/host/catalog-install.js +24 -140
  24. package/dist/host/connection.d.ts +1 -0
  25. package/dist/host/connection.js +2 -0
  26. package/dist/host/credentials.d.ts +1 -2
  27. package/dist/host/credentials.js +2 -67
  28. package/dist/host/dsm-client.d.ts +4 -7
  29. package/dist/host/dsm-client.js +44 -112
  30. package/dist/host/dsm-http.d.ts +1 -1
  31. package/dist/host/dsm-http.js +2 -124
  32. package/dist/host/dsm-port-profile.js +7 -2
  33. package/dist/host/install-plan.d.ts +32 -0
  34. package/dist/host/install-plan.js +157 -0
  35. package/dist/host/package-commands.d.ts +75 -0
  36. package/dist/host/package-commands.js +205 -0
  37. package/dist/host/port-info-lookup.d.ts +1 -1
  38. package/dist/host/port-info-lookup.js +5 -2
  39. package/dist/host/share-lookup.d.ts +3 -2
  40. package/dist/host/share-lookup.js +5 -91
  41. package/dist/host/share-permission-lookup.d.ts +3 -3
  42. package/dist/host/share-permission-lookup.js +18 -30
  43. package/dist/host/types.d.ts +6 -1
  44. package/dist/nas/auth.d.ts +10 -0
  45. package/dist/nas/auth.js +30 -0
  46. package/dist/nas/connection.d.ts +9 -0
  47. package/dist/nas/connection.js +46 -0
  48. package/dist/nas/credentials.d.ts +6 -0
  49. package/dist/nas/credentials.js +22 -0
  50. package/dist/nas/http.d.ts +5 -0
  51. package/dist/nas/http.js +25 -0
  52. package/dist/nas/options.d.ts +13 -0
  53. package/dist/nas/options.js +33 -0
  54. package/dist/nas/session.d.ts +2 -0
  55. package/dist/nas/session.js +6 -0
  56. package/dist/nas/store.d.ts +61 -0
  57. package/dist/nas/store.js +144 -0
  58. package/dist/nas/tls.d.ts +25 -0
  59. package/dist/nas/tls.js +129 -0
  60. package/dist/nas/transfers.d.ts +16 -0
  61. package/dist/nas/transfers.js +76 -0
  62. package/dist/nas/transport.d.ts +17 -0
  63. package/dist/nas/transport.js +143 -0
  64. package/dist/package-source.d.ts +2 -0
  65. package/dist/package-source.js +4 -0
  66. package/dist/skill.js +1 -0
  67. package/dist/spk/inspect.d.ts +1 -0
  68. package/dist/spk/inspect.js +2 -1
  69. package/dist/spk/validate-spk.d.ts +1 -0
  70. package/dist/spk/validate-spk.js +1 -0
  71. package/package.json +2 -2
  72. package/skills/SKILL.md +5 -1
  73. package/skills/reference/ctl.md +6 -4
  74. package/skills/reference/deploy.md +4 -0
  75. package/skills/reference/host-install.md +66 -0
@@ -1,67 +1,2 @@
1
- import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- function findProfile(settings, input) {
5
- if (!input) {
6
- return settings.hosts?.[settings.defaultHost ?? "default"];
7
- }
8
- // 1. Direct match by profile name
9
- if (settings.hosts?.[input]) {
10
- return settings.hosts[input];
11
- }
12
- // 2. Match by URL hostname or exact URL
13
- for (const profile of Object.values(settings.hosts ?? {})) {
14
- if (profile?.url) {
15
- try {
16
- const u = new URL(profile.url);
17
- if (u.hostname === input ||
18
- profile.url === input ||
19
- profile.url.replace(/\/$/, "") === input.replace(/\/$/, "")) {
20
- return profile;
21
- }
22
- }
23
- catch {
24
- // ignore invalid URL
25
- }
26
- }
27
- }
28
- return undefined;
29
- }
30
- export function resolveHostCredentials(hostName) {
31
- const settingsPath = join(homedir(), ".synoagentcli", "setting.json");
32
- let settings;
33
- try {
34
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
35
- }
36
- catch {
37
- return fromEnv(hostName);
38
- }
39
- const profile = findProfile(settings, hostName);
40
- const username = profile?.username ?? profile?.auth?.username;
41
- const password = profile?.password ?? profile?.auth?.password;
42
- if (profile?.url && username && password) {
43
- return {
44
- dsmOrigin: profile.url,
45
- username,
46
- password,
47
- };
48
- }
49
- if (profile?.url) {
50
- return fromEnv(profile.url);
51
- }
52
- return fromEnv(hostName);
53
- }
54
- function fromEnv(urlOverride) {
55
- let dsmOrigin = process.env.DSM_URL;
56
- if (urlOverride) {
57
- dsmOrigin = urlOverride.startsWith("http://") || urlOverride.startsWith("https://")
58
- ? urlOverride
59
- : `https://${urlOverride}:5001`;
60
- }
61
- const password = process.env.DSM_PASSWORD;
62
- const username = process.env.DSM_USERNAME ?? "admin";
63
- if (!dsmOrigin || !password) {
64
- return null;
65
- }
66
- return { dsmOrigin, username, password };
67
- }
1
+ // Compatibility export; NAS connection implementation lives in one module.
2
+ export * from "../nas/credentials.js";
@@ -5,17 +5,15 @@ export interface CreateNasHostOptions {
5
5
  credentials?: HostCredentials;
6
6
  }
7
7
  export declare function createNasHost(options?: CreateNasHostOptions): NasHost | null;
8
- declare function synoagentcli(args: string[], profileName?: string): {
9
- status: number | null;
10
- stdout: string;
11
- stderr: string;
12
- };
13
8
  /**
14
9
  * DSM's User Home read reports the selected volume, not the final share path.
15
10
  * Keep the projection strict: an unexpected response must not become a host
16
11
  * bind to an arbitrary path.
17
12
  */
18
- export declare function homesPathFromDsmUserHomeRead(result: Pick<ReturnType<typeof synoagentcli>, "status" | "stdout">): string | undefined;
13
+ export declare function homesPathFromDsmUserHomeRead(result: {
14
+ status: number | null;
15
+ stdout: string;
16
+ }): string | undefined;
19
17
  /**
20
18
  * DSM reports the CPU vendor, not the ISA. Every x86_64 platform Synology
21
19
  * ships is Intel or AMD; the aarch64 ones are Realtek (rtd1296, rtd1619b) and
@@ -26,4 +24,3 @@ export declare function homesPathFromDsmUserHomeRead(result: Pick<ReturnType<typ
26
24
  * unusable SPK.
27
25
  */
28
26
  export declare function archFamilyForCpuVendor(vendor: string | undefined): NativeArchFamily | undefined;
29
- export {};
@@ -1,11 +1,12 @@
1
- import { spawnSync } from "node:child_process";
2
- import { listShareNamesWithSynoagentcli, lookupSharePathWithSynoagentcli, } from "./share-lookup.js";
3
- import { createSynoagentcliApiCall, fetchSharePermissions, fetchUserGroups, } from "./share-permission-lookup.js";
1
+ import { sessionForCredentials } from "../nas/credentials.js";
2
+ import { CtlError } from '../ctl/errors.js';
3
+ import { listShareNames, lookupSharePath, } from "./share-lookup.js";
4
+ import { createDsmApiCall, fetchSharePermissions, fetchUserGroups, } from "./share-permission-lookup.js";
4
5
  import { fetchPortInfo } from "./port-info-lookup.js";
5
6
  import { dsmFetch } from "./dsm-http.js";
6
7
  import { versionUnsupported } from "../runtime/dsm-version.js";
7
8
  import { fetchDsmPortProfileInventory } from "./dsm-port-profile.js";
8
- import { installPackageFromLocalSpk, fetchInstalledPackageState, packageStateFromRecord, } from "./catalog-install.js";
9
+ import { installPackageFromLocalSpk, fetchInstalledPackageState, } from "./catalog-install.js";
9
10
  import { resolveHostCredentials } from "./credentials.js";
10
11
  export function createNasHost(options = {}) {
11
12
  const credentials = options.credentials ?? resolveHostCredentials(options.host);
@@ -14,18 +15,6 @@ export function createNasHost(options = {}) {
14
15
  }
15
16
  return createDsmNasHost(credentials, options.host);
16
17
  }
17
- function synoagentcli(args, profileName) {
18
- const fullArgs = [...args];
19
- if (profileName) {
20
- fullArgs.push("--host", profileName);
21
- }
22
- const result = spawnSync("synoagentcli", fullArgs, { encoding: "utf8" });
23
- return {
24
- status: result.status,
25
- stdout: result.stdout ?? "",
26
- stderr: result.stderr ?? "",
27
- };
28
- }
29
18
  /**
30
19
  * DSM's User Home read reports the selected volume, not the final share path.
31
20
  * Keep the projection strict: an unexpected response must not become a host
@@ -68,29 +57,10 @@ function createDsmNasHost(credentials, profileName) {
68
57
  const dsmOrigin = credentials.dsmOrigin.replace(/\/$/, "");
69
58
  let cookieHeader = credentials.cookieHeader;
70
59
  const ensureLogin = async () => {
71
- if (cookieHeader) {
72
- return;
60
+ if (!cookieHeader) {
61
+ const session = await sessionForCredentials(credentials);
62
+ cookieHeader = session.cookie;
73
63
  }
74
- const query = new URLSearchParams({
75
- api: "SYNO.API.Auth",
76
- version: "3",
77
- method: "login",
78
- account: credentials.username,
79
- passwd: credentials.password,
80
- session: "FileStation",
81
- format: "cookie",
82
- });
83
- const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi`, {
84
- method: "POST",
85
- headers: { "content-type": "application/x-www-form-urlencoded" },
86
- body: query,
87
- });
88
- const login = (await response.json());
89
- if (!response.ok || login.success !== true)
90
- throw new Error("DSM login failed");
91
- cookieHeader = setCookiePairs(response.headers).join("; ");
92
- if (!cookieHeader)
93
- throw new Error("DSM login returned no session cookie");
94
64
  };
95
65
  /**
96
66
  * One read-only call of the App's own CGI as the administrator. identity:
@@ -129,10 +99,21 @@ function createDsmNasHost(credentials, profileName) {
129
99
  await appCall("identity.logout", appCookie).catch(() => undefined);
130
100
  }
131
101
  };
102
+ const call = async (api, method, version, params = {}) => {
103
+ await ensureLogin();
104
+ const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi`, { method: 'POST', headers: { cookie: cookieHeader ?? '', 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ api, method, version: String(version), ...params }) });
105
+ const body = await response.json();
106
+ if (body?.success !== true)
107
+ throw Object.assign(new Error(`DSM ${api}/${method} failed (${Number(body?.error?.code) || 'invalid response'})`), { code: body?.error?.code });
108
+ return body;
109
+ };
132
110
  return {
133
111
  dsmOrigin,
134
112
  async installFromLocalSpk(input) {
135
113
  await ensureLogin();
114
+ const members = await call('SYNO.Core.Group.Member', 'list', 1, { group: 'administrators', offset: '0', limit: '-1' });
115
+ if (!Array.isArray(members.data?.users) || !members.data.users.some((u) => u.name === credentials.username))
116
+ throw new CtlError('NAS_ADMIN_REQUIRED', 'Package installation requires a DSM administrator account', 'Log in with a DSM administrator account', 4);
136
117
  await installPackageFromLocalSpk({
137
118
  dsmOrigin,
138
119
  cookieHeader: cookieHeader ?? "",
@@ -154,7 +135,9 @@ function createDsmNasHost(credentials, profileName) {
154
135
  return { ready: false, executor_digest: "", policy_digest: "", error: read.error };
155
136
  return explain(read.data);
156
137
  }
157
- catch {
138
+ catch (error) {
139
+ if (error instanceof CtlError)
140
+ throw error;
158
141
  return undefined;
159
142
  }
160
143
  },
@@ -163,7 +146,9 @@ function createDsmNasHost(credentials, profileName) {
163
146
  const read = await readApp(packageIdentity, "portal.status");
164
147
  return read && "data" in read ? read.data : undefined;
165
148
  }
166
- catch {
149
+ catch (error) {
150
+ if (error instanceof CtlError)
151
+ throw error;
167
152
  return undefined;
168
153
  }
169
154
  },
@@ -218,110 +203,57 @@ function createDsmNasHost(credentials, profileName) {
218
203
  const response = await dsmFetch(`${dsmOrigin}/webapi/entry.cgi?api=SYNO.Core.System&version=1&method=info`, { headers: { cookie: cookieHeader ?? "" } });
219
204
  const body = (await response.json());
220
205
  if (body.success !== true) {
206
+ if (Number(body.error?.code) === 105)
207
+ throw new CtlError('NAS_ADMIN_REQUIRED', 'NAS deployment requires a DSM administrator account', 'Log in with a DSM administrator account', 4);
221
208
  return undefined;
222
209
  }
223
210
  return archFamilyForCpuVendor(body.data?.cpu_vendor);
224
211
  },
225
212
  async resolveSharePath(logicalName) {
226
- return lookupSharePathWithSynoagentcli(logicalName, profileName);
213
+ return lookupSharePath(logicalName, call);
227
214
  },
228
215
  async listShareNames() {
229
- return listShareNamesWithSynoagentcli(profileName);
216
+ return listShareNames(call);
230
217
  },
231
218
  async listSharePermissions(logicalName) {
232
- return fetchSharePermissions(logicalName, createSynoagentcliApiCall(profileName));
219
+ return fetchSharePermissions(logicalName, createDsmApiCall(call));
233
220
  },
234
221
  async listUserGroups(userName) {
235
- return fetchUserGroups(userName, createSynoagentcliApiCall(profileName));
222
+ return fetchUserGroups(userName, createDsmApiCall(call));
236
223
  },
237
224
  async listPortInfo() {
238
225
  try {
239
226
  await ensureLogin();
240
227
  return fetchPortInfo(dsmOrigin, cookieHeader ?? "");
241
228
  }
242
- catch {
229
+ catch (error) {
230
+ if (error instanceof CtlError)
231
+ throw error;
243
232
  return undefined;
244
233
  }
245
234
  },
246
235
  async resolveHomesPath() {
247
- try {
248
- const result = synoagentcli([
249
- "dsm",
250
- "control-panel",
251
- "user-group",
252
- "advanced",
253
- "get",
254
- "--section",
255
- "home",
256
- ], profileName);
257
- return homesPathFromDsmUserHomeRead(result);
258
- }
259
- catch {
260
- return undefined;
261
- }
236
+ const body = await call('SYNO.Core.User.Home', 'get', 1);
237
+ return homesPathFromDsmUserHomeRead({ status: 0, stdout: JSON.stringify(body) });
262
238
  },
263
239
  async getInstalledPackageState(packageIdentity) {
264
240
  await ensureLogin();
265
- const viaDsm = await fetchInstalledPackageState(dsmOrigin, cookieHeader ?? "", packageIdentity);
266
- if (viaDsm) {
267
- return viaDsm;
268
- }
269
- const result = synoagentcli(["dsm", "package-center", "get", packageIdentity], profileName);
270
- if (result.status !== 0) {
271
- return undefined;
272
- }
273
- try {
274
- const parsed = JSON.parse(result.stdout);
275
- return packageStateFromRecord(parsed.data);
276
- }
277
- catch {
278
- return undefined;
279
- }
280
- },
281
- async getInstalledPackageVersion(packageIdentity) {
282
- await ensureLogin();
283
- const viaDsm = await fetchInstalledPackageState(dsmOrigin, cookieHeader ?? "", packageIdentity);
284
- if (viaDsm?.version) {
285
- return viaDsm.version;
286
- }
287
- const result = synoagentcli(["dsm", "package-center", "get", packageIdentity], profileName);
288
- if (result.status !== 0) {
289
- return undefined;
290
- }
291
- try {
292
- const parsed = JSON.parse(result.stdout);
293
- return parsed.data?.version;
294
- }
295
- catch {
296
- return undefined;
297
- }
298
- },
299
- async isPackageInstalled(packageIdentity) {
300
- await ensureLogin();
301
- const viaDsm = await fetchInstalledPackageState(dsmOrigin, cookieHeader ?? "", packageIdentity);
302
- if (viaDsm) {
303
- return viaDsm.installed;
304
- }
305
- const result = synoagentcli(["dsm", "package-center", "get", packageIdentity], profileName);
306
- return result.status === 0;
241
+ return fetchInstalledPackageState(dsmOrigin, cookieHeader ?? '', packageIdentity);
307
242
  },
243
+ async getInstalledPackageVersion(packageIdentity) { return (await this.getInstalledPackageState(packageIdentity))?.version; },
244
+ async isPackageInstalled(packageIdentity) { return (await this.getInstalledPackageState(packageIdentity))?.installed === true; },
308
245
  async fetchContainerLogs(packageIdentity) {
309
- const result = synoagentcli(["dsm", "package-center", "status", packageIdentity], profileName);
310
- if (result.status !== 0) {
311
- return [];
312
- }
313
- return result.stdout
314
- .split("\n")
315
- .map((line) => line.trim())
316
- .filter(Boolean)
317
- .map((line) => `[status] ${line}`);
246
+ const state = await this.getInstalledPackageState(packageIdentity);
247
+ return state?.installed ? [`[status] ${state.status ?? 'unknown'} ${state.version ?? ''}`] : [];
318
248
  },
319
249
  async getDsmPortProfileInventory() {
320
250
  try {
321
251
  await ensureLogin();
322
252
  return await fetchDsmPortProfileInventory(dsmOrigin, cookieHeader ?? "");
323
253
  }
324
- catch {
254
+ catch (error) {
255
+ if (error instanceof CtlError)
256
+ throw error;
325
257
  return undefined;
326
258
  }
327
259
  },
@@ -1 +1 @@
1
- export declare function dsmFetch(url: string, init?: RequestInit): Promise<Response>;
1
+ export { dsmFetch, registerDsmSession } from '../nas/http.js';
@@ -1,124 +1,2 @@
1
- import { spawnSync } from "node:child_process";
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- function errorCode(error) {
6
- return error.cause?.code
7
- ?? error.code;
8
- }
9
- function isHostUnreachable(error) {
10
- const code = errorCode(error);
11
- return code === "EHOSTUNREACH" || code === "ENETUNREACH";
12
- }
13
- const UNTRUSTED_CERTIFICATE = new Set([
14
- "DEPTH_ZERO_SELF_SIGNED_CERT",
15
- "SELF_SIGNED_CERT_IN_CHAIN",
16
- "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
17
- "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
18
- "CERT_HAS_EXPIRED",
19
- "ERR_TLS_CERT_ALTNAME_INVALID",
20
- ]);
21
- /** Node reports an untrusted certificate only as "fetch failed"; say what to do. Verification stays on. */
22
- function untrustedCertificate(url, code) {
23
- const origin = new URL(url).origin;
24
- return Object.assign(new Error(`DSM at ${origin} presented a TLS certificate this computer does not trust (${code}). ` +
25
- `Export the NAS certificate or its CA as PEM and run with NODE_EXTRA_CA_CERTS=<that file>, ` +
26
- `or give DSM a certificate whose name matches the host profile. Certificate checks stay on.`), { code: "DSM_TLS_UNTRUSTED" });
27
- }
28
- function headerRecord(headers) {
29
- if (!headers) {
30
- return {};
31
- }
32
- if (headers instanceof Headers) {
33
- return Object.fromEntries(headers.entries());
34
- }
35
- if (Array.isArray(headers)) {
36
- return Object.fromEntries(headers);
37
- }
38
- return { ...headers };
39
- }
40
- export async function dsmFetch(url, init = {}) {
41
- try {
42
- return await fetch(url, init);
43
- }
44
- catch (error) {
45
- const code = errorCode(error);
46
- if (code && UNTRUSTED_CERTIFICATE.has(code)) {
47
- throw untrustedCertificate(url, code);
48
- }
49
- if (!isHostUnreachable(error)) {
50
- throw error;
51
- }
52
- return curlFetch(url, init);
53
- }
54
- }
55
- function curlConfigString(value) {
56
- const escapes = { "\\": "\\\\", '"': '\\"', "\t": "\\t", "\n": "\\n", "\r": "\\r", "\v": "\\v" };
57
- return `"${value.replace(/[\\"\t\n\r\v]/g, (c) => escapes[c])}"`;
58
- }
59
- async function curlFetch(url, init) {
60
- const staging = mkdtempSync(join(tmpdir(), "synopkgland-dsm-http-"));
61
- const headerFile = join(staging, "headers");
62
- const bodyFile = join(staging, "body");
63
- try {
64
- const configFile = join(staging, "config");
65
- const args = ["-sS", "-D", headerFile, "-o", bodyFile, "-w", "%{http_code}", "-X", init.method ?? "GET", "-K", configFile];
66
- // Headers and form text go through a private config file, not argv, and
67
- // form text is sent literally (curl -F would read "@file" and "<file").
68
- const config = [];
69
- const headers = headerRecord(init.headers);
70
- for (const [name, value] of Object.entries(headers)) {
71
- if (name.toLowerCase() === "content-type" && init.body instanceof FormData) {
72
- continue;
73
- }
74
- if (/[\r\n\0]/.test(name + value)) {
75
- throw new Error(`Invalid header value: ${name}`);
76
- }
77
- config.push(`header = ${curlConfigString(`${name}: ${value}`)}`);
78
- }
79
- let stdin;
80
- if (init.body instanceof FormData) {
81
- for (const [name, value] of init.body.entries()) {
82
- if (typeof value === "string") {
83
- if (/[=\0]/.test(name) || value.includes("\0")) {
84
- throw new Error(`Invalid form field: ${name}`);
85
- }
86
- config.push(`form-string = ${curlConfigString(`${name}=${value}`)}`);
87
- continue;
88
- }
89
- const filename = "name" in value && typeof value.name === "string" ? value.name : "upload.bin";
90
- const partPath = join(staging, filename);
91
- writeFileSync(partPath, Buffer.from(await value.arrayBuffer()));
92
- config.push(`form = ${curlConfigString(`${name}=@${partPath};filename=${filename}`)}`);
93
- }
94
- }
95
- else if (typeof init.body === "string") {
96
- stdin = Buffer.from(init.body);
97
- args.push("--data-binary", "@-");
98
- }
99
- else if (init.body) {
100
- stdin = Buffer.from(await new Response(init.body).arrayBuffer());
101
- args.push("--data-binary", "@-");
102
- }
103
- writeFileSync(configFile, config.map((line) => line + "\n").join(""), { mode: 0o600 });
104
- args.push(url);
105
- const result = spawnSync("curl", args, { encoding: "utf8", input: stdin });
106
- if (result.status !== 0) {
107
- throw new Error(result.stderr || `curl exit ${result.status}`);
108
- }
109
- const status = Number((result.stdout ?? "").trim().slice(-3));
110
- const rawHeaders = readFileSync(headerFile, "utf8");
111
- const body = readFileSync(bodyFile);
112
- const responseHeaders = new Headers();
113
- for (const line of rawHeaders.split(/\r?\n/)) {
114
- const idx = line.indexOf(":");
115
- if (idx > 0) {
116
- responseHeaders.append(line.slice(0, idx).trim(), line.slice(idx + 1).trim());
117
- }
118
- }
119
- return new Response(body, { status: Number.isInteger(status) ? status : 502, headers: responseHeaders });
120
- }
121
- finally {
122
- rmSync(staging, { recursive: true, force: true });
123
- }
124
- }
1
+ // Compatibility export; every DSM request uses the shared NAS connection module.
2
+ export { dsmFetch, registerDsmSession } from '../nas/http.js';
@@ -1,3 +1,4 @@
1
+ import { CtlError } from '../ctl/errors.js';
1
2
  import { basename } from "node:path";
2
3
  import { composeProjectName } from "../build-spk/resource-worker.js";
3
4
  import { dsmFetch } from "./dsm-http.js";
@@ -169,7 +170,9 @@ async function fetchJson(dsmOrigin, cookieHeader, params) {
169
170
  try {
170
171
  return await response.json();
171
172
  }
172
- catch {
173
+ catch (error) {
174
+ if (error instanceof CtlError)
175
+ throw error;
173
176
  return undefined;
174
177
  }
175
178
  }
@@ -200,7 +203,9 @@ export async function fetchDsmPortProfileInventory(dsmOrigin, cookieHeader) {
200
203
  });
201
204
  return dsmPortProfileInventoryFromPayloads(portInfoPayload, packageListPayload);
202
205
  }
203
- catch {
206
+ catch (error) {
207
+ if (error instanceof CtlError)
208
+ throw error;
204
209
  return undefined;
205
210
  }
206
211
  }
@@ -0,0 +1,32 @@
1
+ export interface NasTarget {
2
+ arch: string;
3
+ version: string;
4
+ build: number;
5
+ major: number;
6
+ minor: number;
7
+ }
8
+ export interface StoreEntry {
9
+ package: string;
10
+ version: string;
11
+ link: string;
12
+ size: number;
13
+ md5?: string;
14
+ sha256?: string;
15
+ arch?: string | string[];
16
+ os_min_ver?: string;
17
+ os_max_ver?: string;
18
+ deppkgs?: unknown;
19
+ }
20
+ export declare function compareVersions(a: string, b: string): number;
21
+ export declare function archMatches(value: string | string[] | undefined, arch: string): boolean;
22
+ export declare function assertCompatible(info: Record<string, string>, target: NasTarget): void;
23
+ export declare function sourceUrl(source: string, target: NasTarget): URL;
24
+ export declare function selectEntry(entries: StoreEntry[], id: string, version: string | undefined, target: NasTarget): StoreEntry;
25
+ export interface Dependency {
26
+ id: string;
27
+ constraints: Record<string, string>;
28
+ }
29
+ export declare function dependencies(value: unknown): Dependency[];
30
+ export declare function satisfies(version: string, dependency: Dependency): boolean;
31
+ export declare function resolveInstallPlan(root: StoreEntry, entries: StoreEntry[], target: NasTarget, installed: Map<string, string>): StoreEntry[];
32
+ export declare function verifyPins(bytes: Buffer, entry: Pick<StoreEntry, "size" | "md5" | "sha256">): void;