@synopackageland/cli 0.33.0 → 0.33.2
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.
- package/CHANGELOG.md +3 -7
- package/README.md +14 -2
- package/dist/cli.js +7 -0
- package/dist/ctl/cli.js +27 -17
- package/dist/ctl/dsm-auth.d.ts +1 -10
- package/dist/ctl/dsm-auth.js +2 -30
- package/dist/ctl/input.js +1 -1
- package/dist/ctl/service.js +3 -5
- package/dist/ctl/store.d.ts +1 -55
- package/dist/ctl/store.js +2 -121
- package/dist/ctl/tls.d.ts +1 -22
- package/dist/ctl/tls.js +2 -118
- package/dist/ctl/transfers.d.ts +1 -16
- package/dist/ctl/transfers.js +2 -78
- package/dist/ctl/transport.d.ts +1 -14
- package/dist/ctl/transport.js +2 -113
- package/dist/e2e/dsm.d.ts +2 -2
- package/dist/e2e/dsm.js +33 -17
- package/dist/error-registry.js +4 -4
- package/dist/host/connection.d.ts +1 -9
- package/dist/host/connection.js +2 -49
- package/dist/host/credentials.d.ts +1 -4
- package/dist/host/credentials.js +2 -32
- package/dist/host/dsm-client.js +4 -27
- package/dist/host/dsm-http.d.ts +1 -3
- package/dist/host/dsm-http.js +2 -144
- package/dist/host/package-commands.js +3 -3
- package/dist/host/types.d.ts +1 -0
- package/dist/nas/auth.d.ts +10 -0
- package/dist/nas/auth.js +30 -0
- package/dist/nas/connection.d.ts +9 -0
- package/dist/nas/connection.js +46 -0
- package/dist/nas/credentials.d.ts +6 -0
- package/dist/nas/credentials.js +22 -0
- package/dist/nas/http.d.ts +5 -0
- package/dist/nas/http.js +25 -0
- package/dist/nas/options.d.ts +13 -0
- package/dist/nas/options.js +33 -0
- package/dist/nas/session.d.ts +2 -0
- package/dist/nas/session.js +6 -0
- package/dist/nas/store.d.ts +61 -0
- package/dist/nas/store.js +144 -0
- package/dist/nas/tls.d.ts +25 -0
- package/dist/nas/tls.js +129 -0
- package/dist/nas/transfers.d.ts +16 -0
- package/dist/nas/transfers.js +76 -0
- package/dist/nas/transport.d.ts +17 -0
- package/dist/nas/transport.js +143 -0
- package/dist/package-source.d.ts +2 -0
- package/dist/package-source.js +4 -0
- package/dist/standalone/dsm-window.d.ts +2 -0
- package/dist/standalone/dsm-window.js +30 -3
- package/package.json +1 -1
- package/skills/SKILL.md +4 -2
- package/skills/reference/create.md +1 -1
- package/skills/reference/ctl.md +2 -0
- package/skills/reference/deploy.md +2 -0
- package/skills/reference/host-install.md +21 -10
- package/skills/reference/portal.md +1 -1
- package/skills/reference/ui-vue.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.33.
|
|
3
|
+
## 0.33.2 (2026-10-28)
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
- Preview package changes with --dry-run, confirm with --yes and query installed packages without a project directory
|
|
8
|
-
- Trust default NAS certificates on first use, pin the leaf SHA-256 before sending credentials and reject changed certificates until explicitly trusted
|
|
9
|
-
- Distinguish certificate, DNS, connection refusal and timeout errors; support private CAs and expected certificate fingerprints
|
|
10
|
-
- Document interactive and environment-only automation login without saving passwords
|
|
5
|
+
- Load the installed app when a DSM window opens after a package upgrade without a page reload; the loader previously reused the app module the previous build left in the page
|
|
6
|
+
- Give DSM windows and full pages `ctx.uiUpdate.check()`, which reports when the installed package has replaced the UI the page loaded, so the SDK's AppShell can offer a reload after an upgrade
|
package/README.md
CHANGED
|
@@ -282,8 +282,10 @@ for every other frontend. Plain/binary scaffolds explicitly choose new-page; Vue
|
|
|
282
282
|
native `ui.launch` resolves to `dsm-window`; the CLI does not guess a frontend's
|
|
283
283
|
framework from its build command. `new-page` registers DSM `type: "standalone"`,
|
|
284
284
|
so the main menu and Package Center open a full page on the DSM origin.
|
|
285
|
-
`ctx.fullPage`, `ctx.appIcon`, session/SynoToken, launch parameters and entry
|
|
286
|
-
permissions use the same wrapper in both modes.
|
|
285
|
+
`ctx.fullPage`, `ctx.appIcon`, `ctx.uiUpdate`, session/SynoToken, launch parameters and entry
|
|
286
|
+
permissions use the same wrapper in both modes. `ctx.uiUpdate.check()` reports when
|
|
287
|
+
a package upgrade replaced the UI the page loaded; SDK 0.31.5+ AppShell then shows
|
|
288
|
+
a one-line reload notice. The wrapper sets the standalone
|
|
287
289
|
page title, favicon and device-width viewport. It retains ExtJS; `pureEnv` is
|
|
288
290
|
not appropriate, and alternate Portal ports are not needed (`allowAltPort` is omitted).
|
|
289
291
|
|
|
@@ -356,6 +358,16 @@ commands and their schemas. Run the listed alias/command with typed flags,
|
|
|
356
358
|
`--help` and `--dry-run`; writes require `--yes`. There is no mutable current
|
|
357
359
|
context. Use `--host a,b` or `--all-hosts` for bounded parallel operations.
|
|
358
360
|
|
|
361
|
+
NAS HTTPS accepts self-signed, untrusted CA, wrong-name and expired certificates
|
|
362
|
+
by default on every CLI path. The CLI records the leaf SHA-256 for display; a
|
|
363
|
+
changed leaf warns once on stderr, updates that value and continues.
|
|
364
|
+
`host add --strict-tls` stores verification in the profile. `--strict-tls` or
|
|
365
|
+
`--ca <file>` on any NAS command opts into CA, hostname, validity and recorded
|
|
366
|
+
certificate checks. Strict failures use `HOST_TLS_UNTRUSTED` or `HOST_CERT_CHANGED`;
|
|
367
|
+
`host trust <name>` can explicitly update the recorded fingerprint. Passwords
|
|
368
|
+
are sent only over a connection whose certificate was inspected. Public package
|
|
369
|
+
feeds and SPK downloads retain normal HTTPS verification.
|
|
370
|
+
|
|
359
371
|
`ctl` preserves these process exit codes, including with `--json`:
|
|
360
372
|
|
|
361
373
|
| Exit code | Meaning and examples |
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nasFlags, withNasOptions } from './nas/options.js';
|
|
1
2
|
import { upstreamRepository, upstreamMaintainer } from './upstream-attribution.js';
|
|
2
3
|
import { Command, CommanderError } from "commander";
|
|
3
4
|
import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
@@ -289,6 +290,10 @@ async function containerBuild(project, arches, options) {
|
|
|
289
290
|
}
|
|
290
291
|
/** Resolves after dispatch; commands may set process.exitCode. Rejections are failures. */
|
|
291
292
|
export async function runCli(argv) {
|
|
293
|
+
const { args, options } = nasFlags(argv, argv[2] !== "ctl");
|
|
294
|
+
return withNasOptions(options, () => runCliWithin(args));
|
|
295
|
+
}
|
|
296
|
+
async function runCliWithin(argv) {
|
|
292
297
|
if (argv[2] === "host") {
|
|
293
298
|
const { runCtl } = await import("./ctl/cli.js");
|
|
294
299
|
await runCtl(["host", ...argv.slice(3)]);
|
|
@@ -307,6 +312,8 @@ export async function runCli(argv) {
|
|
|
307
312
|
.name("synopkgland")
|
|
308
313
|
.description("Syno Package Land Developer Kit CLI")
|
|
309
314
|
.option("--project <dir>", "App project root")
|
|
315
|
+
.option("--strict-tls", "Verify NAS CA, hostname, validity and recorded certificate")
|
|
316
|
+
.option("--ca <file>", "Verify NAS HTTPS with a PEM CA file")
|
|
310
317
|
.exitOverride();
|
|
311
318
|
const rootOf = (command) => resolve(process.cwd(), command.parent?.opts().project ?? ".");
|
|
312
319
|
const hostOf = (host) => createNasHost({ host });
|
package/dist/ctl/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { nasFlags, withNasOptions, nasOptions } from '../nas/options.js';
|
|
1
2
|
import { probeCertificate, normalizeFingerprint } from './tls.js';
|
|
2
3
|
import { readBounded } from './json.js';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { Writable } from 'node:stream';
|
|
6
6
|
import { CtlError, errorData, ctlError } from './errors.js';
|
|
@@ -10,7 +10,7 @@ import { selectHosts, selectApp, selectOperation, flagName, RESERVED_FLAGS } fro
|
|
|
10
10
|
import { canonicalOrigin, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
11
11
|
import { loginDsm, logoutDsm } from './dsm-auth.js';
|
|
12
12
|
import { publicOperation } from './redaction.js';
|
|
13
|
-
const USAGE = 'synopkgland host add|list|show|login|logout|remove|revoke|default|trust; ctl list [--host a,b|--all-hosts] [--app pkg] [--refresh]; ctl <alias|package> <command...> [typed flags|--input file|-] [--dry-run] [--yes]';
|
|
13
|
+
const USAGE = 'synopkgland host add|list|show|login|logout|remove|revoke|default|trust; ctl list [--host a,b|--all-hosts] [--app pkg] [--refresh]; ctl <alias|package> <command...> [typed flags|--input file|-] [--dry-run] [--yes]; NAS HTTPS accepts any certificate by default; --strict-tls or --ca <file> opts into verification';
|
|
14
14
|
async function prompt(message, secret = false) {
|
|
15
15
|
if (!process.stdin.isTTY)
|
|
16
16
|
throw new CtlError('CTL_INTERACTION_REQUIRED', 'Interactive input required.', 'Use --username and --password-stdin for host authentication.', 2);
|
|
@@ -61,6 +61,16 @@ function example(host, app, op) {
|
|
|
61
61
|
}
|
|
62
62
|
export function listView(host, apps) { return apps.map(app => ({ host, package: app.payload.package, aliases: app.payload.aliases, protocolVersion: app.version, developedBy: app.payload.provenance.developedBy, provenance: 'self-declared', hash: app.sha256, commands: Object.entries(app.payload.commands).map(([id, original]) => { const op = publicOperation(original); return { id, ...op, parameters: op.paramsSchema ?? 'Undescribed parameters; use --input', examples: example(host, app, op) }; }) })); }
|
|
63
63
|
export async function runCtl(argv, io = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const { args, options } = nasFlags(argv);
|
|
66
|
+
return await withNasOptions({ ...options, warn: io.err ?? console.error }, () => runCtlWithin(args, io));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
(io.out ?? console.log)(JSON.stringify({ ok: false, error: errorData(error) }));
|
|
70
|
+
process.exitCode = ctlError(error).exitCode;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function runCtlWithin(argv, io = {}) {
|
|
64
74
|
const args = [...argv], store = io.store ?? new CtlStore(), out = io.out ?? console.log, tty = io.isTTY ?? !!process.stdout.isTTY, ask = io.prompt ?? prompt;
|
|
65
75
|
let json = argv.includes('--json') || !tty;
|
|
66
76
|
const emit = (value) => out(json ? JSON.stringify(value) : JSON.stringify(value, null, 2));
|
|
@@ -98,7 +108,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
98
108
|
if (action === 'add' || action === 'login') {
|
|
99
109
|
const remaining = args.slice(3);
|
|
100
110
|
const url = take(remaining, '--url', true), usernameOption = take(remaining, '--username', true), passwordStdin = !!take(remaining, '--password-stdin'), tunnel = !!take(remaining, '--ssh-tunnel'), noRemember = !!take(remaining, '--no-remember-device'), ca = take(remaining, '--ca', true), fingerprint = take(remaining, '--fingerprint', true);
|
|
101
|
-
if (ca && fingerprint)
|
|
111
|
+
if ((ca || nasOptions().ca) && fingerprint)
|
|
102
112
|
throw new CtlError('CTL_ARGUMENT_INVALID', 'Use --ca or --fingerprint', 'Choose one certificate trust policy');
|
|
103
113
|
if (remaining.length)
|
|
104
114
|
throw new CtlError('CTL_ARGUMENT_INVALID', 'Unknown host options.', 'run: synopkgland host --help', 3);
|
|
@@ -109,25 +119,24 @@ export async function runCtl(argv, io = {}) {
|
|
|
109
119
|
const old = action === 'login' ? store.get(name) : undefined;
|
|
110
120
|
if (action === 'add' && !url)
|
|
111
121
|
throw new CtlError('CTL_URL_REQUIRED', '--url is required.', `run: synopkgland host add ${name} --url https://${name}:5001`);
|
|
112
|
-
const host = old ?? { name, origin: canonicalOrigin(url, tunnel), username: usernameOption ?? process.env.DSM_USERNAME ?? await ask('DSM account: '), approved: {}, ...(tunnel ? { tunnel: true } : {}), ...(ca ? { ca } : {}) };
|
|
122
|
+
const host = old ?? { name, origin: canonicalOrigin(url, tunnel), username: usernameOption ?? process.env.DSM_USERNAME ?? await ask('DSM account: '), approved: {}, ...(tunnel ? { tunnel: true } : {}), ...(nasOptions().ca ? { ca: nasOptions().ca } : {}), ...(nasOptions().strictTls ? { strictTls: true } : {}) };
|
|
113
123
|
if (fingerprint)
|
|
114
124
|
normalizeFingerprint(fingerprint);
|
|
115
|
-
if (old?.fingerprint && fingerprint && normalizeFingerprint(fingerprint) !== old.fingerprint)
|
|
116
|
-
throw new CtlError('HOST_CERT_CHANGED', 'Login cannot replace a certificate pin', `run: synopkgland host trust ${name}`, 6);
|
|
117
125
|
if (!io.requestFor) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
const prior = host.fingerprint;
|
|
127
|
+
const checked = await probeCertificate(host.origin, { ...store.trust(host), expectedFingerprint: fingerprint, timeoutMs });
|
|
128
|
+
if (checked.leafFingerprint) {
|
|
129
|
+
host.fingerprint = checked.leafFingerprint;
|
|
121
130
|
store.save(host);
|
|
122
|
-
if (!checked.trusted)
|
|
123
|
-
(io.err ?? console.error)(`Trusting the NAS certificate ${checked.
|
|
131
|
+
if (!prior && !checked.trusted)
|
|
132
|
+
(io.err ?? console.error)(`Trusting the NAS certificate ${checked.leafFingerprint.slice(0, 16)} (not verified by a trusted CA)`);
|
|
124
133
|
}
|
|
125
134
|
}
|
|
126
135
|
else if (fingerprint)
|
|
127
136
|
host.fingerprint = normalizeFingerprint(fingerprint);
|
|
128
137
|
const previous = old ? await store.credentials.get(store.key(host)) : null;
|
|
129
138
|
const secret = process.env.DSM_PASSWORD ?? (passwordStdin ? readBounded(0, 4096).replace(/[\r\n]+$/, '') : await ask('User password: ', true));
|
|
130
|
-
const session = await loginDsm(host.origin, host.username, secret, { remember: !noRemember, deviceId: previous?.deviceId, otp: () => process.env.DSM_OTP ? Promise.resolve(process.env.DSM_OTP) : ask('DSM OTP: ', true), request: io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs,
|
|
139
|
+
const session = await loginDsm(host.origin, host.username, secret, { remember: !noRemember, deviceId: previous?.deviceId, otp: () => process.env.DSM_OTP ? Promise.resolve(process.env.DSM_OTP) : ask('DSM OTP: ', true), request: io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs, undefined, { ...store.trust(host), expectedFingerprint: fingerprint }) });
|
|
131
140
|
try {
|
|
132
141
|
if (old || host.fingerprint) {
|
|
133
142
|
await store.credentials.set(store.key(host), session);
|
|
@@ -137,7 +146,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
137
146
|
await store.add(host, session);
|
|
138
147
|
}
|
|
139
148
|
catch (e) {
|
|
140
|
-
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs,
|
|
149
|
+
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs, undefined, store.trust(host))).catch(() => { });
|
|
141
150
|
throw e;
|
|
142
151
|
}
|
|
143
152
|
host.lastUsed = new Date().toISOString();
|
|
@@ -151,7 +160,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
151
160
|
throw new CtlError('HOST_CA_CONFIGURED', 'This host uses a configured CA', 'Update the trusted CA file instead of replacing it with a leaf pin', 3);
|
|
152
161
|
if (!host.origin.startsWith('https:'))
|
|
153
162
|
throw new CtlError('HOST_TLS_REQUIRED', 'Certificate trust requires HTTPS', 'Use an HTTPS host registration', 3);
|
|
154
|
-
const checked = await probeCertificate(host.origin, {
|
|
163
|
+
const checked = await probeCertificate(host.origin, { expectedFingerprint: take(args, '--fingerprint', true), name });
|
|
155
164
|
// Get the leaf even when system-trusted, to make an explicit re-pin stable.
|
|
156
165
|
const current = checked.leafFingerprint;
|
|
157
166
|
if (!current)
|
|
@@ -181,7 +190,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
181
190
|
let logoutError;
|
|
182
191
|
if (session?.cookie) {
|
|
183
192
|
try {
|
|
184
|
-
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs,
|
|
193
|
+
await logoutDsm(session, io.requestFor?.(host) ?? createRequest(host.origin, timeoutMs, undefined, store.trust(host)));
|
|
185
194
|
loggedOut = true;
|
|
186
195
|
}
|
|
187
196
|
catch (error) {
|
|
@@ -201,7 +210,8 @@ export async function runCtl(argv, io = {}) {
|
|
|
201
210
|
}
|
|
202
211
|
throw new CtlError('CTL_COMMAND_UNKNOWN', `Unknown host action ${action}.`, 'run: synopkgland host --help', 3, ['add', 'list', 'show', 'login', 'logout', 'remove', 'revoke', 'default', 'trust']);
|
|
203
212
|
}
|
|
204
|
-
const
|
|
213
|
+
const targets = store.targets();
|
|
214
|
+
const names = selectHosts(targets.map(h => h.name), hostOption ?? (allHosts ? undefined : store.defaultName()), allHosts);
|
|
205
215
|
const inputAt = args.indexOf('--input');
|
|
206
216
|
let commonInput;
|
|
207
217
|
if (inputAt >= 0 && args[inputAt + 1] === '-') {
|
|
@@ -214,7 +224,7 @@ export async function runCtl(argv, io = {}) {
|
|
|
214
224
|
}
|
|
215
225
|
const results = await boundedMap(names, 4, async (name) => {
|
|
216
226
|
try {
|
|
217
|
-
const host =
|
|
227
|
+
const host = targets.find(host => host.name === name), ctl = client(host);
|
|
218
228
|
if (args[0] === 'list') {
|
|
219
229
|
if (args.length !== 1)
|
|
220
230
|
throw new CtlError('CTL_ARGUMENT_INVALID', 'Unexpected list arguments.', `run: synopkgland ctl list --host ${name}`);
|
package/dist/ctl/dsm-auth.d.ts
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export interface LoginOptions {
|
|
3
|
-
remember?: boolean;
|
|
4
|
-
deviceId?: string;
|
|
5
|
-
otp: () => Promise<string>;
|
|
6
|
-
request?: JsonRequest;
|
|
7
|
-
ca?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function loginDsm(origin: string, username: string, password: string, options: LoginOptions): Promise<DsmSession>;
|
|
10
|
-
export declare function logoutDsm(session: DsmSession, request?: JsonRequest): Promise<void>;
|
|
1
|
+
export * from "../nas/auth.js";
|
package/dist/ctl/dsm-auth.js
CHANGED
|
@@ -1,30 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export async function loginDsm(origin, username, password, options) {
|
|
4
|
-
const request = options.request ?? createRequest(origin, 30000, options.ca);
|
|
5
|
-
const form = new URLSearchParams({ api: 'SYNO.API.Auth', version: '7', method: 'login', account: username, passwd: password, session: 'DSM', format: 'cookie', enable_syno_token: 'yes', enable_device_token: options.remember === false ? 'no' : 'yes', device_name: 'synopkgland', device_id: options.deviceId ?? '' });
|
|
6
|
-
let result = await request('/webapi/entry.cgi', form);
|
|
7
|
-
if (result?.error?.code === 402) {
|
|
8
|
-
form.delete('session');
|
|
9
|
-
result = await request('/webapi/entry.cgi', form);
|
|
10
|
-
}
|
|
11
|
-
if (result?.error?.code === 403) {
|
|
12
|
-
const otp = await options.otp();
|
|
13
|
-
if (!/^\d{6}$/.test(otp))
|
|
14
|
-
throw new CtlError('CTL_OTP_INVALID', 'OTP must be six digits.', 'run: synopkgland host login <name>', 2);
|
|
15
|
-
form.set('otp_code', otp);
|
|
16
|
-
result = await request('/webapi/entry.cgi', form);
|
|
17
|
-
}
|
|
18
|
-
if (result?.success !== true || typeof result.data?.sid !== 'string' || !result.data.sid || typeof result.data.synotoken !== 'string' || !result.data.synotoken)
|
|
19
|
-
throw new CtlError('CTL_AUTH_FAILED', `DSM login failed (${Number(result?.error?.code) || 'invalid response'}).`, 'run: synopkgland host login <name>', 2);
|
|
20
|
-
if (/[;\r\n\x00]/.test(result.data.sid) || /[\r\n\x00]/.test(result.data.synotoken) || result.data.sid.length > 4096 || result.data.synotoken.length > 4096)
|
|
21
|
-
throw new CtlError('CTL_AUTH_FAILED', 'DSM returned invalid session carriers.', 'Check the DSM endpoint.', 2);
|
|
22
|
-
const returnedDevice = result.data.device_id || result.data.did || options.deviceId;
|
|
23
|
-
const deviceId = options.remember === false ? undefined : typeof returnedDevice === 'string' && returnedDevice.length <= 4096 && !/[\r\n\x00]/.test(returnedDevice) ? returnedDevice : undefined;
|
|
24
|
-
return { origin, username, cookie: `id=${result.data.sid}`, token: result.data.synotoken, ...(deviceId ? { deviceId } : {}) };
|
|
25
|
-
}
|
|
26
|
-
export async function logoutDsm(session, request = createRequest(session.origin)) {
|
|
27
|
-
const body = await request('/webapi/entry.cgi', new URLSearchParams({ api: 'SYNO.API.Auth', version: '7', method: 'logout', SynoToken: session.token }), session);
|
|
28
|
-
if (body?.success !== true)
|
|
29
|
-
throw new CtlError('CTL_LOGOUT_FAILED', 'DSM did not confirm logout.', 'Retry logout or revoke the NAS session.', 2);
|
|
30
|
-
}
|
|
1
|
+
// Compatibility export; NAS connection implementation lives in one module.
|
|
2
|
+
export * from "../nas/auth.js";
|
package/dist/ctl/input.js
CHANGED
|
@@ -34,7 +34,7 @@ export function selectOperation(app, path) {
|
|
|
34
34
|
return { id: found[0], operation: found[1] };
|
|
35
35
|
return choose(path.join(' '), Object.values(app.payload.commands).map(op => op.cli.join(' ')), 'command');
|
|
36
36
|
}
|
|
37
|
-
export const RESERVED_FLAGS = new Set(['host', 'all-hosts', 'json', 'app', 'refresh', 'yes', 'dry-run', 'approve-installed', 'help', 'input', 'wait', 'no-wait', 'timeout']);
|
|
37
|
+
export const RESERVED_FLAGS = new Set(['ca', 'strict-tls', 'host', 'all-hosts', 'json', 'app', 'refresh', 'yes', 'dry-run', 'approve-installed', 'help', 'input', 'wait', 'no-wait', 'timeout']);
|
|
38
38
|
export function flagName(key) { return key.replace(/[A-Z]/g, c => '-' + c.toLowerCase()).replaceAll('_', '-'); }
|
|
39
39
|
export function operationInput(op, args, stdin, helpHint) {
|
|
40
40
|
const fail = (message, allowed) => { throw new CtlError('CTL_PARAMS_INVALID', message, helpHint ?? ('run: synopkgland ctl <package> ' + op.cli.join(' ') + ' --help'), 3, allowed); };
|
package/dist/ctl/service.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
1
|
import { CtlError, errorData, choose } from './errors.js';
|
|
3
2
|
import { APP_COOKIE, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
4
3
|
import { DESCRIPTION_METHOD, validateDescriptor, ajv } from './descriptor.js';
|
|
@@ -17,7 +16,7 @@ export class CtlClient {
|
|
|
17
16
|
constructor(store, host, request, timeoutMs = CTL_TIMEOUT_MS) {
|
|
18
17
|
this.store = store;
|
|
19
18
|
this.host = host;
|
|
20
|
-
this.request = request ?? createRequest(host.origin, () => this.remaining(timeoutMs),
|
|
19
|
+
this.request = request ?? createRequest(host.origin, () => this.remaining(timeoutMs), undefined, store.trust(host));
|
|
21
20
|
}
|
|
22
21
|
until;
|
|
23
22
|
remaining(fallback) { return this.until === undefined ? fallback : Math.max(0, this.until - performance.now()); }
|
|
@@ -150,10 +149,9 @@ export class CtlClient {
|
|
|
150
149
|
}
|
|
151
150
|
return response.data;
|
|
152
151
|
};
|
|
153
|
-
const ca = this.host.ca ? readFileSync(this.host.ca, 'utf8') : undefined;
|
|
154
152
|
const result = await runFlow(operation.flow, params, invoke, { wait: options.wait, timeoutMs: this.remaining(options.timeoutMs ?? CTL_TIMEOUT_MS),
|
|
155
|
-
ndjson: (method, input) => transferRequest(appSession, app.payload.transport.path, method, input, {
|
|
156
|
-
upload: (method, input, flow) => transferRequest(appSession, app.payload.transport.path, method, {}, {
|
|
153
|
+
ndjson: (method, input) => transferRequest(appSession, app.payload.transport.path, method, input, { trust: this.store.trust(this.host), timeoutMs: this.remaining(options.timeoutMs ?? CTL_TIMEOUT_MS) }),
|
|
154
|
+
upload: (method, input, flow) => transferRequest(appSession, app.payload.transport.path, method, {}, { trust: this.store.trust(this.host), timeoutMs: this.remaining(options.timeoutMs ?? CTL_TIMEOUT_MS), file: { path: String(input[flow.fileParam]), parent: String(input[flow.parentParam]), name: String(input[flow.nameParam]) } }) });
|
|
157
155
|
if (operation.resultSchema && !ajv.validate(operation.resultSchema, result))
|
|
158
156
|
throw new CtlError('CTL_RESPONSE_INVALID', 'Result does not match command schema.', 'Check the App version.', 5);
|
|
159
157
|
return output.clean(result, operation.resultSchema);
|
package/dist/ctl/store.d.ts
CHANGED
|
@@ -1,55 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export interface Approval {
|
|
3
|
-
version: string;
|
|
4
|
-
hash?: string;
|
|
5
|
-
}
|
|
6
|
-
export interface Host {
|
|
7
|
-
name: string;
|
|
8
|
-
origin: string;
|
|
9
|
-
username: string;
|
|
10
|
-
fingerprint?: string;
|
|
11
|
-
tunnel?: boolean;
|
|
12
|
-
ca?: string;
|
|
13
|
-
lastUsed?: string;
|
|
14
|
-
approved: Record<string, Approval>;
|
|
15
|
-
}
|
|
16
|
-
export interface CredentialStore {
|
|
17
|
-
get(key: string): Promise<DsmSession | null>;
|
|
18
|
-
set(key: string, value: DsmSession): Promise<void>;
|
|
19
|
-
delete(key: string): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
export declare class OsCredentialStore implements CredentialStore {
|
|
22
|
-
private entry;
|
|
23
|
-
get(key: string): Promise<DsmSession | null>;
|
|
24
|
-
set(key: string, value: DsmSession): Promise<void>;
|
|
25
|
-
delete(key: string): Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
export declare class CtlStore {
|
|
28
|
-
root: string;
|
|
29
|
-
credentials: CredentialStore;
|
|
30
|
-
constructor(root?: string, credentials?: CredentialStore);
|
|
31
|
-
private safe;
|
|
32
|
-
private file;
|
|
33
|
-
key(host: Host): string;
|
|
34
|
-
defaultName(): string | undefined;
|
|
35
|
-
setDefault(name: string): void;
|
|
36
|
-
select(name?: string): Host;
|
|
37
|
-
list(): Host[];
|
|
38
|
-
get(name: string): Host;
|
|
39
|
-
save(host: Host): void;
|
|
40
|
-
add(host: Host, session: DsmSession): Promise<void>;
|
|
41
|
-
session(host: Host): Promise<DsmSession>;
|
|
42
|
-
cacheRoot(host: Host): string;
|
|
43
|
-
cacheFile(host: Host, pkg: string, version: string): string;
|
|
44
|
-
cache(host: Host, pkg: string, version: string): any | null;
|
|
45
|
-
putCache(host: Host, pkg: string, version: string, description: any): void;
|
|
46
|
-
putFailure(host: Host, pkg: string, version: string, failure: unknown): void;
|
|
47
|
-
revoke(name: string, pkg: string): {
|
|
48
|
-
revoked: string;
|
|
49
|
-
cacheCleared: boolean;
|
|
50
|
-
};
|
|
51
|
-
remove(name: string): Promise<{
|
|
52
|
-
removed: string;
|
|
53
|
-
deleted: string[];
|
|
54
|
-
}>;
|
|
55
|
-
}
|
|
1
|
+
export * from "../nas/store.js";
|
package/dist/ctl/store.js
CHANGED
|
@@ -1,121 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { randomUUID, createHash } from 'node:crypto';
|
|
5
|
-
import { CtlError, choose } from './errors.js';
|
|
6
|
-
import { loginDsm } from './dsm-auth.js';
|
|
7
|
-
import { createRequest } from './transport.js';
|
|
8
|
-
import { publicDescriptor } from './redaction.js';
|
|
9
|
-
export class OsCredentialStore {
|
|
10
|
-
async entry(key) { try {
|
|
11
|
-
const { AsyncEntry } = await import('@napi-rs/keyring');
|
|
12
|
-
return new AsyncEntry('synopkgland.ctl', key, { linux: { store: 'secret-service' } });
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
throw new CtlError('CTL_CREDENTIAL_STORE', 'OS secure credential store unavailable.', 'Unlock the OS credential store and retry.', 2);
|
|
16
|
-
} }
|
|
17
|
-
async get(key) { try {
|
|
18
|
-
const raw = await (await this.entry(key)).getPassword();
|
|
19
|
-
return raw ? JSON.parse(raw) : null;
|
|
20
|
-
}
|
|
21
|
-
catch (e) {
|
|
22
|
-
if (e instanceof CtlError)
|
|
23
|
-
throw e;
|
|
24
|
-
throw new CtlError('CTL_CREDENTIAL_STORE', 'Cannot read OS credentials.', 'Unlock the OS credential store and retry.', 2);
|
|
25
|
-
} }
|
|
26
|
-
async set(key, value) { try {
|
|
27
|
-
await (await this.entry(key)).setPassword(JSON.stringify(value));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
throw new CtlError('CTL_CREDENTIAL_STORE', 'Cannot save session in OS secure storage.', 'Unlock the OS credential store and retry.', 2);
|
|
31
|
-
} }
|
|
32
|
-
async delete(key) { try {
|
|
33
|
-
await (await this.entry(key)).deleteCredential();
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
throw new CtlError('CTL_CREDENTIAL_STORE', 'Cannot remove OS credentials.', 'Unlock the OS credential store and retry removal.', 2);
|
|
37
|
-
} }
|
|
38
|
-
}
|
|
39
|
-
export class CtlStore {
|
|
40
|
-
root;
|
|
41
|
-
credentials;
|
|
42
|
-
constructor(root = process.env.SYNOPKGLAND_CTL_DIR ?? join(homedir(), '.synopkgland', 'ctl'), credentials = new OsCredentialStore()) {
|
|
43
|
-
this.root = root;
|
|
44
|
-
this.credentials = credentials;
|
|
45
|
-
}
|
|
46
|
-
safe(name) { if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name))
|
|
47
|
-
throw new CtlError('CTL_HOST_INVALID', 'Invalid host name.', 'Use letters, digits, dots, dashes or underscores.'); return name; }
|
|
48
|
-
file(name) { return join(this.root, 'hosts', this.safe(name) + '.json'); }
|
|
49
|
-
key(host) { return createHash('sha256').update(host.name + '\0' + host.origin + '\0' + host.username).digest('hex'); }
|
|
50
|
-
defaultName() { try {
|
|
51
|
-
return JSON.parse(readFileSync(join(this.root, 'default.json'), 'utf8')).name;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return undefined;
|
|
55
|
-
} }
|
|
56
|
-
setDefault(name) { this.get(name); mkdirSync(this.root, { recursive: true, mode: 0o700 }); const file = join(this.root, 'default.json'), temp = file + '.' + randomUUID(); writeFileSync(temp, JSON.stringify({ name }), { mode: 0o600 }); renameSync(temp, file); }
|
|
57
|
-
select(name) { if (name)
|
|
58
|
-
return this.get(name); const hosts = this.list(); const selected = this.defaultName(); if (selected)
|
|
59
|
-
return this.get(selected); if (hosts.length === 1)
|
|
60
|
-
return hosts[0]; throw new CtlError('CTL_HOST_REQUIRED', hosts.length ? 'Choose a NAS with --host.' : 'No NAS hosts registered.', hosts.length ? 'run: synopkgland host default <name>' : 'run: synopkgland host add <name> --url https://<nas>:5001', 2); }
|
|
61
|
-
list() { const dir = join(this.root, 'hosts'); if (!existsSync(dir))
|
|
62
|
-
return []; return readdirSync(dir).filter(n => n.endsWith('.json')).map(n => JSON.parse(readFileSync(join(dir, n), 'utf8'))).sort((a, b) => a.name.localeCompare(b.name)); }
|
|
63
|
-
get(name) { this.safe(name); const host = this.list().find(h => h.name === name); if (!host)
|
|
64
|
-
return choose(name, this.list().map(h => h.name), 'host'); return host; }
|
|
65
|
-
save(host) { const file = this.file(host.name); mkdirSync(join(this.root, 'hosts'), { recursive: true, mode: 0o700 }); const temp = file + '.' + randomUUID(); writeFileSync(temp, JSON.stringify(host), { mode: 0o600 }); renameSync(temp, file); chmodSync(file, 0o600); }
|
|
66
|
-
async add(host, session) { if (this.list().some(h => h.name === host.name))
|
|
67
|
-
throw new CtlError('CTL_HOST_EXISTS', 'Host name already registered.', `run: synopkgland host login ${host.name}`); await this.credentials.set(this.key(host), session); try {
|
|
68
|
-
this.save(host);
|
|
69
|
-
}
|
|
70
|
-
catch (e) {
|
|
71
|
-
await this.credentials.delete(this.key(host));
|
|
72
|
-
throw e;
|
|
73
|
-
} }
|
|
74
|
-
async session(host) {
|
|
75
|
-
let session;
|
|
76
|
-
if (process.env.DSM_USERNAME && process.env.DSM_PASSWORD) {
|
|
77
|
-
if (process.env.DSM_USERNAME !== host.username)
|
|
78
|
-
throw new CtlError('CTL_CREDENTIAL_MISMATCH', 'Automation account differs from the registered host.', 'Use the registered account or a separately named host.', 2);
|
|
79
|
-
session = await loginDsm(host.origin, host.username, process.env.DSM_PASSWORD, { remember: false, otp: async () => process.env.DSM_OTP ?? '', request: createRequest(host.origin, 30000, host.ca ? readFileSync(host.ca, 'utf8') : undefined, { fingerprint: host.fingerprint, name: host.name }) });
|
|
80
|
-
}
|
|
81
|
-
else
|
|
82
|
-
session = await this.credentials.get(this.key(host));
|
|
83
|
-
if (!session?.cookie || !session.token)
|
|
84
|
-
throw new CtlError('CTL_NOT_AUTHENTICATED', `Host ${host.name} is not logged in.`, `run: synopkgland host login ${host.name}`, 2);
|
|
85
|
-
if (/[\r\n\0]/.test(session.cookie + session.token))
|
|
86
|
-
throw new CtlError('CTL_CREDENTIAL_MISMATCH', 'Stored session carriers are invalid', `run: synopkgland host login ${host.name}`, 2);
|
|
87
|
-
if (session.origin !== host.origin || session.username !== host.username)
|
|
88
|
-
throw new CtlError('CTL_CREDENTIAL_MISMATCH', 'Stored credentials do not match host.', 'Run synopkgland host login again.', 2);
|
|
89
|
-
return session;
|
|
90
|
-
}
|
|
91
|
-
cacheRoot(host) { return join(this.root, 'cache', this.key(host)); }
|
|
92
|
-
cacheFile(host, pkg, version) { return join(this.cacheRoot(host), createHash('sha256').update(pkg + '\0' + version).digest('hex') + '.json'); }
|
|
93
|
-
cache(host, pkg, version) {
|
|
94
|
-
try {
|
|
95
|
-
const entry = JSON.parse(readFileSync(this.cacheFile(host, pkg, version), 'utf8'));
|
|
96
|
-
if (entry.description && !entry.redacted) {
|
|
97
|
-
const safe = publicDescriptor(entry.description);
|
|
98
|
-
if (JSON.stringify(safe) !== JSON.stringify(entry.description)) {
|
|
99
|
-
this.putCache(host, pkg, version, entry.description);
|
|
100
|
-
return { ...entry, description: safe, redacted: true };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return entry;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
putCache(host, pkg, version, description) {
|
|
110
|
-
const safe = publicDescriptor(description);
|
|
111
|
-
const redacted = JSON.stringify(safe) !== JSON.stringify(description);
|
|
112
|
-
mkdirSync(this.cacheRoot(host), { recursive: true, mode: 0o700 });
|
|
113
|
-
const file = this.cacheFile(host, pkg, version), tmp = file + '.' + randomUUID();
|
|
114
|
-
writeFileSync(tmp, JSON.stringify({ hash: description.sha256, description: safe, ...(redacted ? { redacted: true } : {}) }), { mode: 0o600 });
|
|
115
|
-
renameSync(tmp, file);
|
|
116
|
-
}
|
|
117
|
-
putFailure(host, pkg, version, failure) { mkdirSync(this.cacheRoot(host), { recursive: true, mode: 0o700 }); const file = this.cacheFile(host, pkg, version), tmp = file + '.' + randomUUID(); writeFileSync(tmp, JSON.stringify({ failure }), { mode: 0o600 }); renameSync(tmp, file); }
|
|
118
|
-
revoke(name, pkg) { const host = this.get(name); delete host.approved[pkg]; this.save(host); rmSync(this.cacheRoot(host), { recursive: true, force: true }); return { revoked: pkg, cacheCleared: true }; }
|
|
119
|
-
async remove(name) { const host = this.get(name); await this.credentials.delete(this.key(host)); rmSync(this.cacheRoot(host), { recursive: true, force: true }); rmSync(this.file(name)); if (this.defaultName() === name)
|
|
120
|
-
rmSync(join(this.root, 'default.json'), { force: true }); return { removed: name, deleted: ['session', 'SynoToken', 'device token', 'App approvals', 'hash pins', 'list cache', 'host registration'] }; }
|
|
121
|
-
}
|
|
1
|
+
// Compatibility export; NAS connection implementation lives in one module.
|
|
2
|
+
export * from "../nas/store.js";
|
package/dist/ctl/tls.d.ts
CHANGED
|
@@ -1,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { CtlError } from "./errors.js";
|
|
3
|
-
export interface HostTls {
|
|
4
|
-
fingerprint?: string;
|
|
5
|
-
name?: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function normalizeFingerprint(value: string): string;
|
|
8
|
-
export declare function networkError(error: unknown): CtlError;
|
|
9
|
-
export declare function timeoutError(): CtlError;
|
|
10
|
-
/** TLS-only handshake. No HTTP headers, password or session is sent during discovery. */
|
|
11
|
-
export declare function probeCertificate(origin: string, options?: {
|
|
12
|
-
ca?: string;
|
|
13
|
-
fingerprint?: string;
|
|
14
|
-
name?: string;
|
|
15
|
-
timeoutMs?: number;
|
|
16
|
-
}): Promise<{
|
|
17
|
-
fingerprint?: string;
|
|
18
|
-
leafFingerprint?: string;
|
|
19
|
-
trusted: boolean;
|
|
20
|
-
}>;
|
|
21
|
-
/** Release the socket to HTTPS only after matching the leaf pin on this exact connection. */
|
|
22
|
-
export declare function pinnedAgent(origin: string, trust: HostTls, timeoutMs: number): https.Agent;
|
|
1
|
+
export * from "../nas/tls.js";
|
package/dist/ctl/tls.js
CHANGED
|
@@ -1,118 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { isIP } from "node:net";
|
|
4
|
-
import { CtlError } from "./errors.js";
|
|
5
|
-
export function normalizeFingerprint(value) {
|
|
6
|
-
const normalized = value.replaceAll(":", "").toLowerCase();
|
|
7
|
-
if (!/^[a-f0-9]{64}$/.test(normalized))
|
|
8
|
-
throw new CtlError("HOST_FINGERPRINT_INVALID", "A SHA-256 fingerprint must contain 64 hexadecimal digits", "Use the SHA-256 leaf certificate fingerprint", 3);
|
|
9
|
-
return normalized;
|
|
10
|
-
}
|
|
11
|
-
export function networkError(error) {
|
|
12
|
-
if (error instanceof CtlError)
|
|
13
|
-
return error;
|
|
14
|
-
const code = error?.code;
|
|
15
|
-
if (code === "ENOTFOUND" || code === "EAI_AGAIN")
|
|
16
|
-
return new CtlError("HOST_DNS_FAILED", "NAS hostname could not be resolved", "Check the NAS hostname and DNS", 6);
|
|
17
|
-
if (code === "ECONNREFUSED")
|
|
18
|
-
return new CtlError("HOST_CONNECTION_REFUSED", "NAS refused the connection", "Check the DSM port and NAS availability", 6);
|
|
19
|
-
if (code === "ETIMEDOUT" || code === "ESOCKETTIMEDOUT")
|
|
20
|
-
return new CtlError("HOST_TIMEOUT", "NAS connection timed out", "Check the NAS connection and firewall", 6);
|
|
21
|
-
if (code &&
|
|
22
|
-
/CERT|SELF_SIGNED|UNABLE_TO_VERIFY|UNABLE_TO_GET_ISSUER/.test(code))
|
|
23
|
-
return new CtlError("HOST_TLS_UNTRUSTED", "NAS TLS certificate is not trusted", "Use synopkgland host login <name>, --ca, or an expected --fingerprint", 6);
|
|
24
|
-
return new CtlError("CTL_NETWORK", "NAS connection failed", "Check the NAS connection; state-changing requests are not replayed", 6);
|
|
25
|
-
}
|
|
26
|
-
export function timeoutError() {
|
|
27
|
-
return new CtlError("HOST_TIMEOUT", "NAS connection timed out", "Check the NAS connection and firewall", 6);
|
|
28
|
-
}
|
|
29
|
-
function changed(old, current, name) {
|
|
30
|
-
return new CtlError("HOST_CERT_CHANGED", `NAS certificate changed: expected SHA-256 ${old}; received ${current}`, `Verify the new certificate, then run: synopkgland host trust ${name ?? "<name>"}`, 6);
|
|
31
|
-
}
|
|
32
|
-
function socketOptions(origin, ca) {
|
|
33
|
-
const url = new URL(origin), host = url.hostname.replace(/^\[|\]$/g, "");
|
|
34
|
-
return {
|
|
35
|
-
host,
|
|
36
|
-
port: Number(url.port || 443),
|
|
37
|
-
...(isIP(host) ? {} : { servername: host }),
|
|
38
|
-
...(ca ? { ca } : {}),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
/** TLS-only handshake. No HTTP headers, password or session is sent during discovery. */
|
|
42
|
-
export function probeCertificate(origin, options = {}) {
|
|
43
|
-
if (new URL(origin).protocol !== "https:")
|
|
44
|
-
return Promise.resolve({ trusted: true });
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
const socket = tls.connect({
|
|
47
|
-
...socketOptions(origin, options.ca),
|
|
48
|
-
rejectUnauthorized: false,
|
|
49
|
-
});
|
|
50
|
-
const timer = setTimeout(() => socket.destroy(timeoutError()), options.timeoutMs ?? 30000);
|
|
51
|
-
socket.once("error", (e) => reject(networkError(e)));
|
|
52
|
-
socket.once("close", () => clearTimeout(timer));
|
|
53
|
-
socket.once("secureConnect", () => {
|
|
54
|
-
try {
|
|
55
|
-
const raw = socket.getPeerCertificate().fingerprint256;
|
|
56
|
-
if (!raw)
|
|
57
|
-
throw new CtlError("HOST_TLS_UNTRUSTED", "NAS did not provide a leaf certificate", "Check the NAS HTTPS endpoint", 6);
|
|
58
|
-
const fingerprint = normalizeFingerprint(raw);
|
|
59
|
-
if (options.fingerprint &&
|
|
60
|
-
normalizeFingerprint(options.fingerprint) !== fingerprint)
|
|
61
|
-
throw changed(normalizeFingerprint(options.fingerprint), fingerprint, options.name);
|
|
62
|
-
// A caller-provided CA is an explicit trust policy, never a TOFU fallback.
|
|
63
|
-
if (options.ca && !socket.authorized)
|
|
64
|
-
throw new CtlError("HOST_TLS_UNTRUSTED", "NAS certificate was not verified by the supplied CA", "Check the CA file and NAS hostname", 6);
|
|
65
|
-
resolve({
|
|
66
|
-
trusted: socket.authorized,
|
|
67
|
-
leafFingerprint: fingerprint,
|
|
68
|
-
fingerprint: options.fingerprint || !socket.authorized ? fingerprint : undefined,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
reject(e);
|
|
73
|
-
}
|
|
74
|
-
finally {
|
|
75
|
-
socket.destroy();
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
/** Release the socket to HTTPS only after matching the leaf pin on this exact connection. */
|
|
81
|
-
export function pinnedAgent(origin, trust, timeoutMs) {
|
|
82
|
-
const agent = new https.Agent({ keepAlive: false });
|
|
83
|
-
agent.createConnection = (_options, callback) => {
|
|
84
|
-
let done = false;
|
|
85
|
-
const socket = tls.connect({
|
|
86
|
-
...socketOptions(origin),
|
|
87
|
-
rejectUnauthorized: false,
|
|
88
|
-
});
|
|
89
|
-
const timer = setTimeout(() => socket.destroy(timeoutError()), timeoutMs);
|
|
90
|
-
const finish = (error) => {
|
|
91
|
-
if (done)
|
|
92
|
-
return;
|
|
93
|
-
done = true;
|
|
94
|
-
clearTimeout(timer);
|
|
95
|
-
if (error) {
|
|
96
|
-
socket.destroy();
|
|
97
|
-
callback?.(error, null);
|
|
98
|
-
}
|
|
99
|
-
else
|
|
100
|
-
callback?.(null, socket);
|
|
101
|
-
};
|
|
102
|
-
socket.once("error", (e) => finish(networkError(e)));
|
|
103
|
-
socket.once("secureConnect", () => {
|
|
104
|
-
try {
|
|
105
|
-
const current = normalizeFingerprint(socket.getPeerCertificate().fingerprint256 ?? "");
|
|
106
|
-
const old = normalizeFingerprint(trust.fingerprint);
|
|
107
|
-
if (old !== current)
|
|
108
|
-
throw changed(old, current, trust.name);
|
|
109
|
-
finish();
|
|
110
|
-
}
|
|
111
|
-
catch (e) {
|
|
112
|
-
finish(e);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return undefined;
|
|
116
|
-
};
|
|
117
|
-
return agent;
|
|
118
|
-
}
|
|
1
|
+
// Compatibility export; NAS connection implementation lives in one module.
|
|
2
|
+
export * from "../nas/tls.js";
|
package/dist/ctl/transfers.d.ts
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { type DsmSession } from './transport.js';
|
|
3
|
-
export declare function localFile(file: unknown): {
|
|
4
|
-
file: string;
|
|
5
|
-
size: number;
|
|
6
|
-
};
|
|
7
|
-
export declare function transferRequest(session: DsmSession, path: string, method: string, params: Record<string, unknown>, options?: {
|
|
8
|
-
file?: {
|
|
9
|
-
path: string;
|
|
10
|
-
parent: string;
|
|
11
|
-
name: string;
|
|
12
|
-
};
|
|
13
|
-
ca?: string;
|
|
14
|
-
timeoutMs?: number;
|
|
15
|
-
trust?: HostTls;
|
|
16
|
-
}): Promise<any>;
|
|
1
|
+
export * from "../nas/transfers.js";
|