@synopackageland/cli 0.41.9 → 0.41.11
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 +29 -0
- package/README.md +61 -2
- package/dist/cli.js +2 -4
- package/dist/compose/registry-mirrors.d.ts +3 -0
- package/dist/compose/registry-mirrors.js +38 -0
- package/dist/e2e/app-open.d.ts +5 -0
- package/dist/e2e/dsm.d.ts +3 -1
- package/dist/e2e/dsm.js +109 -7
- package/dist/error-registry.js +8 -0
- package/dist/host/catalog-install.d.ts +12 -3
- package/dist/host/catalog-install.js +202 -82
- package/dist/host/container-pull.d.ts +18 -0
- package/dist/host/container-pull.js +120 -0
- package/dist/host/dsm-client.js +3 -0
- package/dist/host/dsm-compound.js +10 -3
- package/dist/host/package-commands.js +4 -0
- package/dist/host/package-diagnostics.d.ts +5 -0
- package/dist/host/package-diagnostics.js +62 -0
- package/dist/images/pin.js +6 -16
- package/dist/log.js +5 -8
- package/dist/nas/auth.js +1 -1
- package/dist/nas/connection.js +1 -1
- package/dist/nas/http.d.ts +1 -1
- package/dist/nas/http.js +2 -2
- package/dist/nas/store.js +1 -1
- package/dist/nas/timeouts.d.ts +8 -0
- package/dist/nas/timeouts.js +25 -0
- package/dist/nas/tls.js +2 -1
- package/dist/nas/transport.d.ts +2 -2
- package/dist/nas/transport.js +11 -6
- package/dist/standalone/compose.js +2 -4
- package/package.json +1 -1
- package/skills/SKILL.md +6 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/** Diagnostic text is untrusted and can contain daemon URLs or echoed credentials. */
|
|
6
|
+
export function maskDiagnostic(text) {
|
|
7
|
+
return text.replace(/https?:\/\/[^\s"'<>]+/gi, value => {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(value);
|
|
10
|
+
if (!url.username && !url.password && !url.search && !url.hash)
|
|
11
|
+
return value;
|
|
12
|
+
url.username = '';
|
|
13
|
+
url.password = '';
|
|
14
|
+
url.search = '';
|
|
15
|
+
url.hash = '';
|
|
16
|
+
return url.href;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '<redacted URL>';
|
|
20
|
+
}
|
|
21
|
+
}).replace(/\b(password|passwd|token|synotoken|_sid|authorization|cookie|set-cookie)\s*[:=]\s*[^\r\n]+/gi, '$1=<redacted>');
|
|
22
|
+
}
|
|
23
|
+
function pathFor(origin, packageId) {
|
|
24
|
+
const key = createHash('sha256').update(new URL(origin).origin + '\0' + packageId).digest('hex');
|
|
25
|
+
return join(process.env.SYNOPKGLAND_DIAGNOSTICS_DIR ?? join(homedir(), '.cache/synopkgland/diagnostics'), key + '.json');
|
|
26
|
+
}
|
|
27
|
+
export function savePullFailure(origin, packageId, message) {
|
|
28
|
+
try {
|
|
29
|
+
const file = pathFor(origin, packageId);
|
|
30
|
+
if (!message) {
|
|
31
|
+
rmSync(file, { force: true });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const dir = file.slice(0, file.lastIndexOf('/'));
|
|
35
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
36
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
37
|
+
writeFileSync(temporary, JSON.stringify({ source: 'CLI NAS pre-pull', observedAt: new Date().toISOString(), message: maskDiagnostic(message) }), { mode: 0o600 });
|
|
38
|
+
renameSync(temporary, file);
|
|
39
|
+
}
|
|
40
|
+
catch { /* Optional diagnostic retention must not mask the install result. */ }
|
|
41
|
+
}
|
|
42
|
+
function readPullFailure(origin, packageId) {
|
|
43
|
+
try {
|
|
44
|
+
const value = JSON.parse(readFileSync(pathFor(origin, packageId), 'utf8'));
|
|
45
|
+
return { source: value.source, observedAt: value.observedAt, message: maskDiagnostic(String(value.message)) };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function packageDiagnostics(packageId, state, origin) {
|
|
52
|
+
const clean = (value) => typeof value === 'string' ? maskDiagnostic(value) : Array.isArray(value) ? value.map(clean)
|
|
53
|
+
: value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([key, v]) => [key, /password|passwd|token|sid|authorization|cookie/i.test(key) ? '<redacted>' : clean(v)])) : value;
|
|
54
|
+
const worker = clean(state?.error?.workerMsg ?? []);
|
|
55
|
+
const messages = (value) => typeof value === 'string' ? [value] : Array.isArray(value) ? value.flatMap(messages)
|
|
56
|
+
: value && typeof value === 'object' ? Object.values(value).flatMap(messages) : [];
|
|
57
|
+
const lastComposeError = messages(worker).filter(line => /compose|pull|docker|x509|tls|certificate|registry/i.test(line)).at(-1);
|
|
58
|
+
return { packageIdentity: packageId, readiness: state?.status, error: clean(state?.error), worker_msg: worker,
|
|
59
|
+
statusCode: state?.statusCode, statusDescription: clean(state?.statusDescription),
|
|
60
|
+
...(lastComposeError ? { lastComposeError } : {}),
|
|
61
|
+
...(state?.status === 'broken' || state?.error?.code ? { lastPullError: readPullFailure(origin, packageId) } : {}), redacted: true };
|
|
62
|
+
}
|
package/dist/images/pin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { registryBaseUrl } from "../compose/registry-mirrors.js";
|
|
2
3
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { isScalar, parseDocument } from "yaml";
|
|
4
5
|
/**
|
|
@@ -73,18 +74,6 @@ export function parseImageReference(image) {
|
|
|
73
74
|
}
|
|
74
75
|
return { registry, repository, tag, digest, explicitTag };
|
|
75
76
|
}
|
|
76
|
-
function registryBaseUrl(registry) {
|
|
77
|
-
if (registry === "docker.io" || registry === "index.docker.io") {
|
|
78
|
-
return "https://registry-1.docker.io";
|
|
79
|
-
}
|
|
80
|
-
const host = registry.startsWith("[")
|
|
81
|
-
? registry.slice(0, registry.indexOf("]") + 1)
|
|
82
|
-
: registry.split(":")[0];
|
|
83
|
-
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]") {
|
|
84
|
-
return `http://${registry}`;
|
|
85
|
-
}
|
|
86
|
-
return `https://${registry}`;
|
|
87
|
-
}
|
|
88
77
|
function parseBearerChallenge(header) {
|
|
89
78
|
if (!header) {
|
|
90
79
|
return null;
|
|
@@ -152,12 +141,13 @@ export async function resolveImageDigest(image, deps = {}) {
|
|
|
152
141
|
if (!response.ok) {
|
|
153
142
|
throw new ImageDigestError(`registry answered HTTP ${response.status} for ${image}`);
|
|
154
143
|
}
|
|
144
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
145
|
+
const digest = `sha256:${createHash("sha256").update(body).digest("hex")}`;
|
|
155
146
|
const header = response.headers.get("docker-content-digest");
|
|
156
|
-
if (header && DIGEST_VALUE.test(header)) {
|
|
157
|
-
|
|
147
|
+
if (header && (!DIGEST_VALUE.test(header) || header.toLowerCase() !== digest)) {
|
|
148
|
+
throw new ImageDigestError(`registry content digest mismatch for ${image}`);
|
|
158
149
|
}
|
|
159
|
-
|
|
160
|
-
return `sha256:${createHash("sha256").update(body).digest("hex")}`;
|
|
150
|
+
return digest;
|
|
161
151
|
}
|
|
162
152
|
/** Keep the source's quote style so only the reference text changes. */
|
|
163
153
|
function quoteLike(node, value) {
|
package/dist/log.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadStandaloneManifest } from "./standalone/manifest.js";
|
|
2
2
|
import { resolveDeployIdentity } from "./deploy/identity.js";
|
|
3
|
+
import { maskDiagnostic, packageDiagnostics } from './host/package-diagnostics.js';
|
|
3
4
|
export async function followProjectLogs(options) {
|
|
4
5
|
const identity = resolveDeployIdentity(options.project, { dev: options.dev });
|
|
5
6
|
const installed = await options.host.isPackageInstalled(identity.packageId);
|
|
@@ -10,12 +11,8 @@ export async function followProjectLogs(options) {
|
|
|
10
11
|
const info = await options.host.getInstalledPackageState?.(identity.packageId);
|
|
11
12
|
return {
|
|
12
13
|
ok: true,
|
|
13
|
-
lines,
|
|
14
|
-
diagnostics:
|
|
15
|
-
packageIdentity: identity.packageId,
|
|
16
|
-
readiness: info?.status,
|
|
17
|
-
redacted: true,
|
|
18
|
-
},
|
|
14
|
+
lines: lines.map(maskDiagnostic),
|
|
15
|
+
diagnostics: packageDiagnostics(identity.packageId, info, options.host.dsmOrigin),
|
|
19
16
|
};
|
|
20
17
|
}
|
|
21
18
|
export async function followStandaloneLogs(options) {
|
|
@@ -31,7 +28,7 @@ export async function followStandaloneLogs(options) {
|
|
|
31
28
|
const info = await options.host.getInstalledPackageState?.(packageIdentity);
|
|
32
29
|
return {
|
|
33
30
|
ok: true,
|
|
34
|
-
lines,
|
|
35
|
-
diagnostics:
|
|
31
|
+
lines: lines.map(maskDiagnostic),
|
|
32
|
+
diagnostics: packageDiagnostics(packageIdentity, info, options.host.dsmOrigin),
|
|
36
33
|
};
|
|
37
34
|
}
|
package/dist/nas/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CtlError } from '../ctl/errors.js';
|
|
2
2
|
import { createRequest } from './transport.js';
|
|
3
3
|
export async function loginDsm(origin, username, password, options) {
|
|
4
|
-
const request = options.request ?? createRequest(origin,
|
|
4
|
+
const request = options.request ?? createRequest(origin, undefined, options.ca);
|
|
5
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
6
|
let result = await request('/webapi/entry.cgi', form);
|
|
7
7
|
if (result?.error?.code === 402) {
|
package/dist/nas/connection.js
CHANGED
|
@@ -6,7 +6,7 @@ export async function connectNas(name) {
|
|
|
6
6
|
const credentials = resolveHostCredentials(name);
|
|
7
7
|
const session = await credentials.loadSession();
|
|
8
8
|
registerDsmSession(session, credentials.hostName, credentials.ca, credentials.fingerprint, credentials.tls);
|
|
9
|
-
const request = createRequest(session.origin,
|
|
9
|
+
const request = createRequest(session.origin, undefined, credentials.ca, credentials.tls);
|
|
10
10
|
return {
|
|
11
11
|
name: credentials.hostName,
|
|
12
12
|
session,
|
package/dist/nas/http.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ import { type DsmSession } from './transport.js';
|
|
|
2
2
|
import type { HostTls } from './tls.js';
|
|
3
3
|
export declare function registerDsmSession(session: DsmSession, name: string, ca?: string, fingerprint?: string, policy?: HostTls): void;
|
|
4
4
|
/** All DSM fetch callers, including session-only automation, use the NAS socket policy. */
|
|
5
|
-
export declare function dsmFetch(url: string, init?: RequestInit): Promise<Response>;
|
|
5
|
+
export declare function dsmFetch(url: string, init?: RequestInit, timeoutMs?: number): Promise<Response>;
|
package/dist/nas/http.js
CHANGED
|
@@ -9,12 +9,12 @@ export function registerDsmSession(session, name, ca, fingerprint, policy) {
|
|
|
9
9
|
policies.set(session.origin, trust);
|
|
10
10
|
}
|
|
11
11
|
/** All DSM fetch callers, including session-only automation, use the NAS socket policy. */
|
|
12
|
-
export async function dsmFetch(url, init = {}) {
|
|
12
|
+
export async function dsmFetch(url, init = {}, timeoutMs) {
|
|
13
13
|
const headers = new Headers(init.headers), origin = new URL(url).origin;
|
|
14
14
|
const cookie = headers.get('cookie') ?? '';
|
|
15
15
|
// App cookies can accompany a DSM cookie; keep its token and TLS policy.
|
|
16
16
|
const binding = [...bindings.values()].find(b => b.session.origin === origin && cookie.split(/;\s*/).includes(b.session.cookie));
|
|
17
|
-
const response = await sessionFetch(url, { ...init, headers }, binding ? { ...binding.session, cookie } : undefined, undefined, binding?.trust ?? policies.get(origin));
|
|
17
|
+
const response = await sessionFetch(url, { ...init, headers }, binding ? { ...binding.session, cookie } : undefined, undefined, binding?.trust ?? policies.get(origin), timeoutMs);
|
|
18
18
|
if (response.status >= 300 && response.status < 400)
|
|
19
19
|
throw new CtlError('CTL_HTTP_ERROR', `HTTP ${response.status}; redirects are never followed`, 'Check the NAS URL', 6);
|
|
20
20
|
if (binding && new URL(url).pathname === '/webapi/entry.cgi') {
|
package/dist/nas/store.js
CHANGED
|
@@ -99,7 +99,7 @@ export class CtlStore {
|
|
|
99
99
|
if (!options.storedOnly && process.env.DSM_USERNAME && process.env.DSM_PASSWORD) {
|
|
100
100
|
if (process.env.DSM_USERNAME !== host.username)
|
|
101
101
|
throw new CtlError('CTL_CREDENTIAL_MISMATCH', 'Automation account differs from the registered host.', 'Use the registered account or a separately named host.', 2);
|
|
102
|
-
session = await loginDsm(host.origin, host.username, process.env.DSM_PASSWORD, { remember: false, otp: async () => process.env.DSM_OTP ?? '', request: createRequest(host.origin,
|
|
102
|
+
session = await loginDsm(host.origin, host.username, process.env.DSM_PASSWORD, { remember: false, otp: async () => process.env.DSM_OTP ?? '', request: createRequest(host.origin, undefined, undefined, this.trust(host)) });
|
|
103
103
|
}
|
|
104
104
|
else
|
|
105
105
|
session = await this.credentials.get(this.key(host));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const dsmRequestTimeoutMs: () => number;
|
|
2
|
+
export declare const dsmOperationTimeoutMs: () => number;
|
|
3
|
+
export declare const dsmUninstallTimeoutMs: () => number;
|
|
4
|
+
export declare const dsmInstallTimeoutMs: () => number;
|
|
5
|
+
export declare const dsmPollIntervalMs: () => number;
|
|
6
|
+
export declare const dsmPullStallTimeoutMs: () => number;
|
|
7
|
+
export declare function isHostTimeout(error: unknown): boolean;
|
|
8
|
+
export declare function dsmApiTimeoutMs(api: string | null, method: string | null): number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CtlError } from '../ctl/errors.js';
|
|
2
|
+
/** Seconds in the existing DSM_* automation environment (also loaded from .env). */
|
|
3
|
+
function seconds(name, fallback) {
|
|
4
|
+
const value = process.env[name];
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return fallback * 1000;
|
|
7
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > 86400)
|
|
8
|
+
throw new CtlError('DSM_TIMEOUT_INVALID', `${name} must be a whole number of seconds from 1 to 86400`, 'Set a positive DSM timeout budget');
|
|
9
|
+
return Number(value) * 1000;
|
|
10
|
+
}
|
|
11
|
+
export const dsmRequestTimeoutMs = () => seconds('DSM_REQUEST_TIMEOUT_SECONDS', 180);
|
|
12
|
+
export const dsmOperationTimeoutMs = () => seconds('DSM_OPERATION_TIMEOUT_SECONDS', 600);
|
|
13
|
+
export const dsmUninstallTimeoutMs = () => seconds('DSM_UNINSTALL_TIMEOUT_SECONDS', 1800);
|
|
14
|
+
export const dsmInstallTimeoutMs = () => seconds('DSM_INSTALL_TIMEOUT_SECONDS', 1800);
|
|
15
|
+
export const dsmPollIntervalMs = () => seconds('DSM_POLL_INTERVAL_SECONDS', 5);
|
|
16
|
+
export const dsmPullStallTimeoutMs = () => seconds('DSM_IMAGE_PULL_STALL_TIMEOUT_SECONDS', 1200);
|
|
17
|
+
export function isHostTimeout(error) {
|
|
18
|
+
return error instanceof CtlError && error.code === 'HOST_TIMEOUT';
|
|
19
|
+
}
|
|
20
|
+
export function dsmApiTimeoutMs(api, method) {
|
|
21
|
+
const longOperation = api === 'SYNO.Entry.Request' || api === 'SYNO.Core.Package.Uninstallation' ||
|
|
22
|
+
(api === 'SYNO.Core.Package.Installation' && ['upload', 'install'].includes(method ?? '')) ||
|
|
23
|
+
(api === 'SYNO.Core.Package.Control' && ['start', 'stop'].includes(method ?? ''));
|
|
24
|
+
return longOperation ? dsmOperationTimeoutMs() : dsmRequestTimeoutMs();
|
|
25
|
+
}
|
package/dist/nas/tls.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dsmRequestTimeoutMs } from './timeouts.js';
|
|
1
2
|
import tls from "node:tls";
|
|
2
3
|
import https from "node:https";
|
|
3
4
|
import { isIP } from "node:net";
|
|
@@ -47,7 +48,7 @@ export function probeCertificate(origin, options = {}) {
|
|
|
47
48
|
...socketOptions(origin, options.ca),
|
|
48
49
|
rejectUnauthorized: false,
|
|
49
50
|
});
|
|
50
|
-
const timer = setTimeout(() => socket.destroy(timeoutError()), options.timeoutMs ??
|
|
51
|
+
const timer = setTimeout(() => socket.destroy(timeoutError()), options.timeoutMs ?? dsmRequestTimeoutMs());
|
|
51
52
|
socket.once("error", (e) => reject(networkError(e)));
|
|
52
53
|
socket.once("close", () => clearTimeout(timer));
|
|
53
54
|
socket.once("secureConnect", () => {
|
package/dist/nas/transport.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface DsmSession {
|
|
|
11
11
|
}
|
|
12
12
|
export type JsonRequest = (path: string, body: URLSearchParams | Record<string, unknown>, session?: DsmSession) => Promise<any>;
|
|
13
13
|
export declare function canonicalOrigin(input: string, tunnel?: boolean): string;
|
|
14
|
-
export declare function createRequest(origin: string, timeoutMs?: number | (() => number), ca?: string, trust?: HostTls): JsonRequest;
|
|
15
|
-
export declare function sessionFetch(url: string, init: RequestInit, session: DsmSession | undefined, ca?: string, trust?: HostTls): Promise<Response>;
|
|
14
|
+
export declare function createRequest(origin: string, timeoutMs?: number | (() => number) | undefined, ca?: string, trust?: HostTls): JsonRequest;
|
|
15
|
+
export declare function sessionFetch(url: string, init: RequestInit, session: DsmSession | undefined, ca?: string, trust?: HostTls, timeoutMs?: number): Promise<Response>;
|
|
16
16
|
/** The only Node HTTP dispatch for NAS endpoints, including streaming transfers. */
|
|
17
17
|
export declare function nasHttpRequest(url: URL, options: http.RequestOptions, timeoutMs: number, trust: HostTls | undefined, listener: (res: http.IncomingMessage) => void): http.ClientRequest;
|
package/dist/nas/transport.js
CHANGED
|
@@ -6,6 +6,7 @@ import { assertSessionActive } from './session.js';
|
|
|
6
6
|
import { nasOptions } from './options.js';
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { CtlError } from '../ctl/errors.js';
|
|
9
|
+
import { dsmRequestTimeoutMs, dsmApiTimeoutMs } from './timeouts.js';
|
|
9
10
|
export const CTL_TIMEOUT_MS = 62000;
|
|
10
11
|
export const APP_COOKIE = Symbol("ctlAppCookie");
|
|
11
12
|
export function canonicalOrigin(input, tunnel = false) {
|
|
@@ -20,11 +21,12 @@ export function canonicalOrigin(input, tunnel = false) {
|
|
|
20
21
|
throw new CtlError('CTL_URL_INVALID', 'Use HTTPS, or explicit --ssh-tunnel for a loopback HTTP tunnel.', 'run: synopkgland host add <name> --url https://<nas>:5001');
|
|
21
22
|
return url.origin;
|
|
22
23
|
}
|
|
23
|
-
export function createRequest(origin, timeoutMs =
|
|
24
|
+
export function createRequest(origin, timeoutMs = undefined, ca, trust) {
|
|
24
25
|
return async (path, body, session) => {
|
|
25
26
|
if (!path.startsWith('/') || path.startsWith('//') || path.includes('?') || path.includes('#') || path.includes('\\') || path.includes('%') || path.split('/').some(p => p === '.' || p === '..'))
|
|
26
27
|
throw new CtlError('CTL_PATH_INVALID', 'Unsafe request path.', 'run: synopkgland ctl list --refresh');
|
|
27
|
-
const
|
|
28
|
+
const params = body instanceof URLSearchParams ? body : undefined;
|
|
29
|
+
const remaining = typeof timeoutMs === 'function' ? timeoutMs() : timeoutMs ?? (path === '/webapi/entry.cgi' ? dsmApiTimeoutMs(params?.get('api') ?? null, params?.get('method') ?? null) : CTL_TIMEOUT_MS);
|
|
28
30
|
if (remaining <= 0)
|
|
29
31
|
throw new CtlError('CTL_NETWORK', 'Request budget exhausted before dispatch.', 'Check App state before retrying writes.', 6);
|
|
30
32
|
const url = new URL(path, origin);
|
|
@@ -91,7 +93,10 @@ export function createRequest(origin, timeoutMs = CTL_TIMEOUT_MS, ca, trust) {
|
|
|
91
93
|
});
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
|
-
export async function sessionFetch(url, init, session, ca, trust) {
|
|
96
|
+
export async function sessionFetch(url, init, session, ca, trust, timeoutMs) {
|
|
97
|
+
const targetUrl = new URL(url);
|
|
98
|
+
const params = init.body instanceof URLSearchParams ? init.body : targetUrl.searchParams;
|
|
99
|
+
timeoutMs ??= targetUrl.pathname === '/webapi/entry.cgi' ? dsmApiTimeoutMs(params.get('api'), params.get('method')) : dsmRequestTimeoutMs();
|
|
95
100
|
if (session && new URL(url).origin !== session.origin)
|
|
96
101
|
throw new CtlError('CTL_ORIGIN_MISMATCH', 'Credentials cannot cross NAS origins', 'Check the registered NAS origin', 4);
|
|
97
102
|
const headers = new Headers(init.headers);
|
|
@@ -102,14 +107,14 @@ export async function sessionFetch(url, init, session, ca, trust) {
|
|
|
102
107
|
const request = new Request(url, { ...init, headers }), bytes = init.body ? Buffer.from(await request.arrayBuffer()) : undefined;
|
|
103
108
|
return new Promise((resolve, reject) => {
|
|
104
109
|
const target = new URL(url);
|
|
105
|
-
const req = nasHttpRequest(target, { method: request.method, headers: Object.fromEntries(request.headers) },
|
|
110
|
+
const req = nasHttpRequest(target, { method: request.method, headers: Object.fromEntries(request.headers) }, timeoutMs, trustWithCa(trust, ca), res => {
|
|
106
111
|
const chunks = [];
|
|
107
112
|
let length = 0;
|
|
108
113
|
res.on('data', c => { length += c.length; if (length > 16 * 1024 * 1024)
|
|
109
114
|
res.destroy(new CtlError('CTL_RESPONSE_TOO_LARGE', 'DSM response exceeds 16 MiB', 'Use a narrower operation', 5));
|
|
110
115
|
else
|
|
111
116
|
chunks.push(c); });
|
|
112
|
-
res.on('error',
|
|
117
|
+
res.on('error', e => reject(e instanceof CtlError ? e : new CtlError('DSM_NETWORK', 'DSM response interrupted', 'Check the NAS connection', 6)));
|
|
113
118
|
res.on('end', () => { const headers = new Headers(); for (const [k, v] of Object.entries(res.headers))
|
|
114
119
|
for (const x of Array.isArray(v) ? v : [v])
|
|
115
120
|
if (x !== undefined)
|
|
@@ -121,7 +126,7 @@ export async function sessionFetch(url, init, session, ca, trust) {
|
|
|
121
126
|
else
|
|
122
127
|
request.signal.addEventListener('abort', abort, { once: true });
|
|
123
128
|
req.once('close', () => request.signal.removeEventListener('abort', abort));
|
|
124
|
-
const timer = setTimeout(() => req.destroy(timeoutError()),
|
|
129
|
+
const timer = setTimeout(() => req.destroy(timeoutError()), timeoutMs);
|
|
125
130
|
req.once('close', () => clearTimeout(timer));
|
|
126
131
|
req.on('timeout', () => req.destroy(timeoutError()));
|
|
127
132
|
req.on('error', e => reject(networkError(e)));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { composeRegistryMirrors } from "../compose/registry-mirrors.js";
|
|
1
2
|
import { portDefaults } from './install-ports.js';
|
|
2
3
|
import { validComposeAppData } from "../runtime/compose-data.js";
|
|
3
4
|
import { composeDiagnosisMessages, renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
|
|
@@ -126,10 +127,7 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
|
|
|
126
127
|
install_settings: context.installSettings,
|
|
127
128
|
install_ports: context.installPorts,
|
|
128
129
|
resolve_images: context.resolveImages === true,
|
|
129
|
-
registry_mirrors: context.resolveImages
|
|
130
|
-
process.env.SYNOPKGLAND_COMPOSE_REGISTRY_MIRRORS
|
|
131
|
-
? JSON.parse(process.env.SYNOPKGLAND_COMPOSE_REGISTRY_MIRRORS)
|
|
132
|
-
: undefined,
|
|
130
|
+
registry_mirrors: context.resolveImages ? composeRegistryMirrors() : undefined,
|
|
133
131
|
architecture: context.architecture,
|
|
134
132
|
image_proofs: context.imageProofs,
|
|
135
133
|
}),
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -316,8 +316,12 @@ Use `appOpenSmoke` from `@synopackageland/cli/e2e`; keep one content-specific
|
|
|
316
316
|
reuses the existing DSM credentials/profiles/tunnel, and registers administrator,
|
|
317
317
|
normal-user and fresh anonymous tests. Configure `DSM_E2E_ACCOUNT_ADMIN` and
|
|
318
318
|
`DSM_E2E_ACCOUNT_NORMAL` as private account files; a missing normal profile and
|
|
319
|
-
credentials is a reported skip.
|
|
320
|
-
|
|
319
|
+
credentials is a reported skip. Smoke needs `e2e`, `@e2e-dev/web` and Chromium;
|
|
320
|
+
`ai` and `@ai-sdk/openai` are required only for `magpieAgent()`. Declare an optional
|
|
321
|
+
interstitial as `preReady: {clickIfVisible: {text: "Continue on this device"}}`.
|
|
322
|
+
For `entry: admin-only`, N must see the SDK gateway denial page or an explicit
|
|
323
|
+
DSM denial/launcher privilege signal without an App window. Timeout alone is not
|
|
324
|
+
denial. No App content or ready marker may be present in the DOM; use
|
|
321
325
|
`content` for a separate App shell marker. Other entry kinds require N to
|
|
322
326
|
reach `ready`; grant Application Privileges as needed. Preserve the manifest's
|
|
323
327
|
entry policy when running the smoke.
|