@synopackageland/cli 0.3.1 → 0.3.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/dist/attestation/app-package.js +8 -1
- package/dist/attestation/platform-service.d.ts +70 -0
- package/dist/attestation/platform-service.js +250 -0
- package/dist/bin.js +0 -0
- package/dist/build-spk/generator.js +202 -10
- package/dist/build-spk/resource-worker.d.ts +14 -0
- package/dist/build-spk/resource-worker.js +140 -0
- package/dist/cli.js +69 -10
- package/dist/compose/analyze.d.ts +41 -1
- package/dist/compose/analyze.js +458 -18
- package/dist/contract/load.d.ts +1 -0
- package/dist/contract/load.js +1 -1
- package/dist/contract/posix-all-shares.d.ts +16 -0
- package/dist/contract/posix-all-shares.js +63 -0
- package/dist/contract/schema-validate.js +26 -1
- package/dist/contract/validate.js +57 -0
- package/dist/contract/web-launch.d.ts +6 -0
- package/dist/contract/web-launch.js +25 -0
- package/dist/deploy/deploy.d.ts +1 -0
- package/dist/deploy/deploy.js +37 -2
- package/dist/deploy/homes-values.d.ts +19 -0
- package/dist/deploy/homes-values.js +29 -0
- package/dist/deploy/param-values.d.ts +21 -0
- package/dist/deploy/param-values.js +51 -0
- package/dist/deploy/spk-version.d.ts +5 -0
- package/dist/deploy/spk-version.js +50 -18
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +594 -0
- package/dist/error-collector.d.ts +2 -0
- package/dist/error-collector.js +46 -0
- package/dist/error-registry.d.ts +7 -0
- package/dist/error-registry.js +827 -0
- package/dist/host/broker-upgrade-preflight.d.ts +61 -0
- package/dist/host/broker-upgrade-preflight.js +156 -0
- package/dist/host/catalog-install.d.ts +12 -0
- package/dist/host/catalog-install.js +83 -17
- package/dist/host/credentials.js +35 -5
- package/dist/host/dsm-client.d.ts +12 -0
- package/dist/host/dsm-client.js +56 -1
- package/dist/host/dsm-compound.d.ts +37 -0
- package/dist/host/dsm-compound.js +120 -0
- package/dist/host/fake-nas.d.ts +1 -0
- package/dist/host/fake-nas.js +3 -0
- package/dist/host/types.d.ts +4 -0
- package/dist/publish/curated-release.js +3 -3
- package/dist/publish/platform-service-release.d.ts +60 -0
- package/dist/publish/platform-service-release.js +333 -0
- package/dist/publish/preflight.js +14 -1
- package/dist/release-bin.js +5 -1
- package/dist/templates/hello-files/compose.yaml +2 -0
- package/dist/templates/hello-web/compose.yaml +14 -3
- package/dist/templates/hello-web/synology-app.yaml +1 -1
- package/dist/templates/hello-web-page/compose.yaml +14 -3
- package/dist/templates/hello-web-page/synology-app.yaml +1 -1
- package/dist/test-host.d.ts +2 -0
- package/dist/test-host.js +15 -2
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +14 -3
- package/package.json +2 -2
- package/schema/doctor.schema.json +38 -0
- package/schema/synology-app.schema.json +1 -1
- package/skills/reference/create.md +19 -0
- package/skills/reference/diagnose.md +35 -2
- package/skills/reference/files.md +10 -0
- package/templates/hello-files/compose.yaml +2 -0
- package/templates/hello-web/compose.yaml +14 -3
- package/templates/hello-web/synology-app.yaml +1 -1
- package/templates/hello-web-page/compose.yaml +14 -3
- package/templates/hello-web-page/synology-app.yaml +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const BROKER_UPGRADE_TARGET_HOST = "10.17.83.200";
|
|
2
|
+
export type DsmUpgradeResponseKind = "error-4552" | "success" | "consent" | "unknown";
|
|
3
|
+
export interface ConsentDialogEvidence {
|
|
4
|
+
consentDialog: {
|
|
5
|
+
dependers: string[];
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface BrokerUpgradeDependerBaseline {
|
|
9
|
+
packageIdentity: string;
|
|
10
|
+
version: string;
|
|
11
|
+
status: string;
|
|
12
|
+
running: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface BrokerUpgradeDepender extends BrokerUpgradeDependerBaseline {
|
|
15
|
+
healthCheckOk: boolean;
|
|
16
|
+
a23Classified: boolean;
|
|
17
|
+
dataSnapshotOk: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface BrokerUpgradePreflightInput {
|
|
20
|
+
targetHost: string;
|
|
21
|
+
broker: {
|
|
22
|
+
version: string;
|
|
23
|
+
status: string;
|
|
24
|
+
running: boolean;
|
|
25
|
+
};
|
|
26
|
+
dependers: BrokerUpgradeDepender[];
|
|
27
|
+
previousSpk: {
|
|
28
|
+
available: boolean;
|
|
29
|
+
sha256?: string;
|
|
30
|
+
};
|
|
31
|
+
canonicalizeGateSatisfied: boolean;
|
|
32
|
+
catalogEntry: {
|
|
33
|
+
version: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export type PreflightDecision = {
|
|
37
|
+
ok: true;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
codes: string[];
|
|
41
|
+
};
|
|
42
|
+
export interface Evaluate4552BranchInput {
|
|
43
|
+
response: unknown;
|
|
44
|
+
dependers: BrokerUpgradeDependerBaseline[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Classify only already-parsed DSM evidence. Strings are deliberately never
|
|
48
|
+
* parsed or searched: a textual mention of 4552 is not a DSM error response.
|
|
49
|
+
*/
|
|
50
|
+
export declare function classifyDsmUpgradeResponse(raw: unknown): DsmUpgradeResponseKind;
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate every local prerequisite without performing an upgrade or
|
|
53
|
+
* proposing a stop/start action. A missing or malformed prerequisite is a
|
|
54
|
+
* failure, even when another part of the input looks healthy.
|
|
55
|
+
*/
|
|
56
|
+
export declare function evaluateBrokerUpgradePreflight(input: BrokerUpgradePreflightInput): PreflightDecision;
|
|
57
|
+
/**
|
|
58
|
+
* The 4552 path is entered only for a numeric DSM error and a non-empty,
|
|
59
|
+
* fully identified running baseline inventory.
|
|
60
|
+
*/
|
|
61
|
+
export declare function evaluate4552Branch(input: Evaluate4552BranchInput): PreflightDecision;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { comparePackageVersions } from "@synopackageland/package-source/index-manifest";
|
|
2
|
+
export const BROKER_UPGRADE_TARGET_HOST = "10.17.83.200";
|
|
3
|
+
/**
|
|
4
|
+
* Classify only already-parsed DSM evidence. Strings are deliberately never
|
|
5
|
+
* parsed or searched: a textual mention of 4552 is not a DSM error response.
|
|
6
|
+
*/
|
|
7
|
+
export function classifyDsmUpgradeResponse(raw) {
|
|
8
|
+
if (!isRecord(raw)) {
|
|
9
|
+
return "unknown";
|
|
10
|
+
}
|
|
11
|
+
if (isRecord(raw.error) && raw.error.code === 4552) {
|
|
12
|
+
return "error-4552";
|
|
13
|
+
}
|
|
14
|
+
const hasError = Object.prototype.hasOwnProperty.call(raw, "error") &&
|
|
15
|
+
raw.error !== null &&
|
|
16
|
+
raw.error !== undefined;
|
|
17
|
+
if (raw.success === true && !hasError) {
|
|
18
|
+
return "success";
|
|
19
|
+
}
|
|
20
|
+
if (isConsentDialogEvidence(raw)) {
|
|
21
|
+
return "consent";
|
|
22
|
+
}
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Evaluate every local prerequisite without performing an upgrade or
|
|
27
|
+
* proposing a stop/start action. A missing or malformed prerequisite is a
|
|
28
|
+
* failure, even when another part of the input looks healthy.
|
|
29
|
+
*/
|
|
30
|
+
export function evaluateBrokerUpgradePreflight(input) {
|
|
31
|
+
const candidate = isRecord(input) ? input : {};
|
|
32
|
+
const codes = [];
|
|
33
|
+
const addCode = (code) => {
|
|
34
|
+
if (!codes.includes(code)) {
|
|
35
|
+
codes.push(code);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (candidate.targetHost !== BROKER_UPGRADE_TARGET_HOST) {
|
|
39
|
+
addCode("PREFLIGHT_WRONG_TARGET");
|
|
40
|
+
}
|
|
41
|
+
const broker = candidate.broker;
|
|
42
|
+
if (!isRecord(broker)) {
|
|
43
|
+
addCode("PREFLIGHT_BROKER_NOT_RUNNING");
|
|
44
|
+
addCode("PREFLIGHT_BROKER_VERSION_INVALID");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
if (broker.running !== true || broker.status !== "running") {
|
|
48
|
+
addCode("PREFLIGHT_BROKER_NOT_RUNNING");
|
|
49
|
+
}
|
|
50
|
+
if (!isNonEmptyString(broker.version)) {
|
|
51
|
+
addCode("PREFLIGHT_BROKER_VERSION_INVALID");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const dependers = candidate.dependers;
|
|
55
|
+
if (!Array.isArray(dependers)) {
|
|
56
|
+
addCode("PREFLIGHT_DEPENDER_INVENTORY_INVALID");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
for (const depender of dependers) {
|
|
60
|
+
if (!isRecord(depender)) {
|
|
61
|
+
addCode("PREFLIGHT_DEPENDER_INVENTORY_INVALID");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isNonEmptyString(depender.packageIdentity) || !isNonEmptyString(depender.version)) {
|
|
65
|
+
addCode("PREFLIGHT_DEPENDER_INVENTORY_INVALID");
|
|
66
|
+
}
|
|
67
|
+
if (depender.running !== true) {
|
|
68
|
+
addCode("PREFLIGHT_DEPENDER_NOT_RUNNING");
|
|
69
|
+
}
|
|
70
|
+
if (depender.status !== "running") {
|
|
71
|
+
addCode("PREFLIGHT_DEPENDER_UNSAFE_STATUS");
|
|
72
|
+
}
|
|
73
|
+
if (depender.healthCheckOk !== true) {
|
|
74
|
+
addCode("PREFLIGHT_DEPENDER_HEALTHCHECK_FAILED");
|
|
75
|
+
}
|
|
76
|
+
if (depender.a23Classified !== true) {
|
|
77
|
+
addCode("PREFLIGHT_DEPENDER_A23_UNCLASSIFIED");
|
|
78
|
+
}
|
|
79
|
+
if (depender.dataSnapshotOk !== true) {
|
|
80
|
+
addCode("PREFLIGHT_DEPENDER_DATA_SNAPSHOT_MISSING");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const previousSpk = candidate.previousSpk;
|
|
85
|
+
if (!isRecord(previousSpk) || previousSpk.available !== true) {
|
|
86
|
+
addCode("PREFLIGHT_PREVIOUS_SPK_UNAVAILABLE");
|
|
87
|
+
}
|
|
88
|
+
else if (!isSha256(previousSpk.sha256)) {
|
|
89
|
+
addCode("PREFLIGHT_PREVIOUS_SPK_DIGEST_INVALID");
|
|
90
|
+
}
|
|
91
|
+
if (candidate.canonicalizeGateSatisfied !== true) {
|
|
92
|
+
addCode("PREFLIGHT_CANONICALIZE_GATE_UNSATISFIED");
|
|
93
|
+
}
|
|
94
|
+
const catalogEntry = candidate.catalogEntry;
|
|
95
|
+
if (!isRecord(catalogEntry) || !isNonEmptyString(catalogEntry.version)) {
|
|
96
|
+
addCode("PREFLIGHT_CATALOG_VERSION_INVALID");
|
|
97
|
+
}
|
|
98
|
+
else if (!isRecord(broker) || !isNonEmptyString(broker.version)) {
|
|
99
|
+
// The broker version gate is already reported above; without both version
|
|
100
|
+
// strings there is no safe comparison to perform.
|
|
101
|
+
addCode("PREFLIGHT_VERSION_COMPARISON_INVALID");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const comparison = comparePackageVersions(catalogEntry.version, broker.version);
|
|
105
|
+
if (!Number.isFinite(comparison)) {
|
|
106
|
+
addCode("PREFLIGHT_VERSION_COMPARISON_INVALID");
|
|
107
|
+
}
|
|
108
|
+
else if (comparison <= 0) {
|
|
109
|
+
addCode("PREFLIGHT_CATALOG_VERSION_NOT_NEWER");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return decisionFromCodes(codes);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The 4552 path is entered only for a numeric DSM error and a non-empty,
|
|
116
|
+
* fully identified running baseline inventory.
|
|
117
|
+
*/
|
|
118
|
+
export function evaluate4552Branch(input) {
|
|
119
|
+
const candidate = isRecord(input) ? input : {};
|
|
120
|
+
const codes = [];
|
|
121
|
+
if (classifyDsmUpgradeResponse(candidate.response) !== "error-4552") {
|
|
122
|
+
codes.push("PREFLIGHT_RESPONSE_NOT_4552");
|
|
123
|
+
}
|
|
124
|
+
const dependers = candidate.dependers;
|
|
125
|
+
if (!Array.isArray(dependers) ||
|
|
126
|
+
dependers.length === 0 ||
|
|
127
|
+
dependers.some((depender) => !isRunningBaseline(depender))) {
|
|
128
|
+
codes.push("PREFLIGHT_DEPENDER_INVENTORY_INCOMPLETE");
|
|
129
|
+
}
|
|
130
|
+
return decisionFromCodes(codes);
|
|
131
|
+
}
|
|
132
|
+
function decisionFromCodes(codes) {
|
|
133
|
+
return codes.length === 0 ? { ok: true } : { ok: false, codes };
|
|
134
|
+
}
|
|
135
|
+
function isRunningBaseline(value) {
|
|
136
|
+
return (isRecord(value) &&
|
|
137
|
+
isNonEmptyString(value.packageIdentity) &&
|
|
138
|
+
isNonEmptyString(value.version) &&
|
|
139
|
+
value.status === "running" &&
|
|
140
|
+
value.running === true);
|
|
141
|
+
}
|
|
142
|
+
function isConsentDialogEvidence(value) {
|
|
143
|
+
const dialog = value.consentDialog;
|
|
144
|
+
return (isRecord(dialog) &&
|
|
145
|
+
Array.isArray(dialog.dependers) &&
|
|
146
|
+
dialog.dependers.every((depender) => typeof depender === "string"));
|
|
147
|
+
}
|
|
148
|
+
function isRecord(value) {
|
|
149
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
150
|
+
}
|
|
151
|
+
function isNonEmptyString(value) {
|
|
152
|
+
return typeof value === "string" && value.length > 0;
|
|
153
|
+
}
|
|
154
|
+
function isSha256(value) {
|
|
155
|
+
return typeof value === "string" && /^[0-9a-fA-F]{64}$/.test(value);
|
|
156
|
+
}
|
|
@@ -23,11 +23,23 @@ export interface InstalledPackageState {
|
|
|
23
23
|
installed: boolean;
|
|
24
24
|
version?: string;
|
|
25
25
|
status?: string;
|
|
26
|
+
error?: {
|
|
27
|
+
code?: number;
|
|
28
|
+
description?: string;
|
|
29
|
+
workerMsg?: unknown[];
|
|
30
|
+
};
|
|
31
|
+
brokenBy?: string;
|
|
32
|
+
stage?: string;
|
|
33
|
+
lastStage?: string;
|
|
34
|
+
statusCode?: number;
|
|
35
|
+
statusDescription?: string;
|
|
36
|
+
statusOrigin?: string;
|
|
26
37
|
}
|
|
27
38
|
export interface UploadedCatalogPackage {
|
|
28
39
|
id: string;
|
|
29
40
|
taskId: string;
|
|
30
41
|
}
|
|
42
|
+
export declare function packageStateFromRecord(value: unknown): InstalledPackageState;
|
|
31
43
|
export declare function packageStateFromListPayload(payload: unknown, packageId: string): InstalledPackageState | undefined;
|
|
32
44
|
export declare function fetchInstalledPackageState(dsmOrigin: string, cookieHeader: string, packageId: string): Promise<InstalledPackageState | undefined>;
|
|
33
45
|
export declare function waitForInstalledVersion(readState: () => InstalledPackageState | undefined | Promise<InstalledPackageState | undefined>, options: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { dsmFetch } from "./dsm-http.js";
|
|
4
|
+
import { assertDsmCompoundResponse } from "./dsm-compound.js";
|
|
4
5
|
function normalizeDeppkgs(deppkgs) {
|
|
5
6
|
if (typeof deppkgs === "string") {
|
|
6
7
|
const match = /^([^><=]+)(>=|>|<=|<|==)(.+)$/.exec(deppkgs.trim());
|
|
@@ -27,7 +28,9 @@ async function dsmWebApi(dsmOrigin, cookieHeader, params, options = {}) {
|
|
|
27
28
|
});
|
|
28
29
|
const body = (await response.json());
|
|
29
30
|
const errorCode = body.error?.code;
|
|
30
|
-
if (!body.success &&
|
|
31
|
+
if (!body.success &&
|
|
32
|
+
!options.allowFailure &&
|
|
33
|
+
!(errorCode && options.allowErrorCodes?.includes(errorCode))) {
|
|
31
34
|
throw new Error(`DSM ${params.api}/${params.method} failed: ${JSON.stringify(body.error ?? body)}`);
|
|
32
35
|
}
|
|
33
36
|
return body;
|
|
@@ -92,29 +95,90 @@ function readInstalledPackageState(packageId, profileName) {
|
|
|
92
95
|
}
|
|
93
96
|
try {
|
|
94
97
|
const parsed = JSON.parse(result.stdout);
|
|
95
|
-
|
|
96
|
-
return { installed: false, version: parsed.data?.version, status: parsed.data?.status };
|
|
97
|
-
}
|
|
98
|
-
return { installed: true, version: parsed.data.version, status: parsed.data.status };
|
|
98
|
+
return packageStateFromRecord(parsed.data);
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
101
|
return undefined;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
function record(value) {
|
|
105
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
106
|
+
? value
|
|
107
|
+
: {};
|
|
108
|
+
}
|
|
109
|
+
function stringValue(...values) {
|
|
110
|
+
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
111
|
+
}
|
|
112
|
+
function numberValue(...values) {
|
|
113
|
+
return values.find((value) => typeof value === "number" && Number.isFinite(value));
|
|
114
|
+
}
|
|
115
|
+
function packageError(value) {
|
|
116
|
+
const source = record(value);
|
|
117
|
+
const code = numberValue(source.code);
|
|
118
|
+
const description = stringValue(source.description);
|
|
119
|
+
const workerMsg = Array.isArray(source.worker_msg)
|
|
120
|
+
? source.worker_msg
|
|
121
|
+
: Array.isArray(source.workerMsg)
|
|
122
|
+
? source.workerMsg
|
|
123
|
+
: undefined;
|
|
124
|
+
if (code === undefined && description === undefined && workerMsg === undefined) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const error = {};
|
|
128
|
+
if (code !== undefined)
|
|
129
|
+
error.code = code;
|
|
130
|
+
if (description !== undefined)
|
|
131
|
+
error.description = description;
|
|
132
|
+
if (workerMsg !== undefined)
|
|
133
|
+
error.workerMsg = workerMsg;
|
|
134
|
+
return error;
|
|
135
|
+
}
|
|
136
|
+
export function packageStateFromRecord(value) {
|
|
137
|
+
const match = record(value);
|
|
138
|
+
const additional = record(match.additional);
|
|
139
|
+
const installedInfo = record(additional.installed_info ?? match.installedInfo);
|
|
140
|
+
const installedPath = stringValue(installedInfo.path, match.installedPath);
|
|
141
|
+
const version = stringValue(match.version);
|
|
142
|
+
const status = stringValue(additional.status, match.rawStatus, match.status);
|
|
143
|
+
const error = packageError(additional.error ?? match.error);
|
|
144
|
+
const state = {
|
|
145
|
+
installed: Boolean(installedPath) || match.installed === true,
|
|
146
|
+
};
|
|
147
|
+
if (version !== undefined)
|
|
148
|
+
state.version = version;
|
|
149
|
+
if (status !== undefined)
|
|
150
|
+
state.status = status;
|
|
151
|
+
if (error !== undefined)
|
|
152
|
+
state.error = error;
|
|
153
|
+
const brokenBy = stringValue(additional.broken_by, additional.brokenBy, match.broken_by, match.brokenBy);
|
|
154
|
+
if (brokenBy !== undefined)
|
|
155
|
+
state.brokenBy = brokenBy;
|
|
156
|
+
const stage = stringValue(additional.stage, match.stage);
|
|
157
|
+
if (stage !== undefined)
|
|
158
|
+
state.stage = stage;
|
|
159
|
+
const lastStage = stringValue(additional.last_stage, additional.lastStage, match.last_stage, match.lastStage);
|
|
160
|
+
if (lastStage !== undefined)
|
|
161
|
+
state.lastStage = lastStage;
|
|
162
|
+
const statusCode = numberValue(additional.status_code, additional.statusCode, match.status_code, match.statusCode, match.rawStatusCode);
|
|
163
|
+
if (statusCode !== undefined)
|
|
164
|
+
state.statusCode = statusCode;
|
|
165
|
+
const statusDescription = stringValue(additional.status_description, additional.statusDescription, match.status_description, match.statusDescription, match.rawStatusDescription);
|
|
166
|
+
if (statusDescription !== undefined)
|
|
167
|
+
state.statusDescription = statusDescription;
|
|
168
|
+
const statusOrigin = stringValue(additional.status_origin, additional.statusOrigin, match.status_origin, match.statusOrigin, match.rawStatusOrigin);
|
|
169
|
+
if (statusOrigin !== undefined)
|
|
170
|
+
state.statusOrigin = statusOrigin;
|
|
171
|
+
return state;
|
|
172
|
+
}
|
|
104
173
|
export function packageStateFromListPayload(payload, packageId) {
|
|
105
|
-
const
|
|
106
|
-
const data =
|
|
174
|
+
const root = payload && typeof payload === "object" ? payload : undefined;
|
|
175
|
+
const data = root?.data && typeof root.data === "object" ? root.data : root;
|
|
107
176
|
const packages = Array.isArray(data?.packages) ? data.packages : [];
|
|
108
177
|
const match = packages.find((item) => item && typeof item === "object" && item.id === packageId);
|
|
109
178
|
if (!match) {
|
|
110
179
|
return { installed: false };
|
|
111
180
|
}
|
|
112
|
-
|
|
113
|
-
const installedPath = additional?.installed_info?.path;
|
|
114
|
-
const installed = Boolean(installedPath) || match.installed === true;
|
|
115
|
-
const status = typeof additional?.status === "string" ? additional.status : undefined;
|
|
116
|
-
const version = typeof match.version === "string" ? match.version : undefined;
|
|
117
|
-
return { installed, version, status };
|
|
181
|
+
return packageStateFromRecord(match);
|
|
118
182
|
}
|
|
119
183
|
export async function fetchInstalledPackageState(dsmOrigin, cookieHeader, packageId) {
|
|
120
184
|
try {
|
|
@@ -265,7 +329,7 @@ export async function installPackageFromLocalSpk(options) {
|
|
|
265
329
|
const installed = dsmState?.installed === true || isPackageInstalled(options.packageId, options.profileName);
|
|
266
330
|
const uploaded = await uploadLocalSpk(options, sid);
|
|
267
331
|
const compound = JSON.stringify(catalogCompoundSteps(uploaded, { version: options.version, size: readFileSync(options.spkPath).byteLength }, { blupgrade: installed, extraValues: options.extraValues }));
|
|
268
|
-
await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
332
|
+
const compoundResponse = await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
269
333
|
api: "SYNO.Entry.Request",
|
|
270
334
|
version: "1",
|
|
271
335
|
method: "request",
|
|
@@ -273,7 +337,8 @@ export async function installPackageFromLocalSpk(options) {
|
|
|
273
337
|
mode: jsonApiParam("sequential"),
|
|
274
338
|
compound,
|
|
275
339
|
_sid: sid,
|
|
276
|
-
});
|
|
340
|
+
}, { allowFailure: true });
|
|
341
|
+
assertDsmCompoundResponse(compoundResponse);
|
|
277
342
|
await waitForInstalledVersion(async () => (await fetchInstalledPackageState(options.dsmOrigin, options.cookieHeader, options.packageId)) ??
|
|
278
343
|
readInstalledPackageState(options.packageId, options.profileName), { packageId: options.packageId, expectedVersion: options.version });
|
|
279
344
|
}
|
|
@@ -303,7 +368,7 @@ async function uploadCatalogSpk(options, entry, sid) {
|
|
|
303
368
|
async function installCatalogViaUpload(options, entry, sid, installed) {
|
|
304
369
|
const uploaded = await uploadCatalogSpk(options, entry, sid);
|
|
305
370
|
const compound = JSON.stringify(catalogCompoundSteps(uploaded, entry, { blupgrade: installed, extraValues: options.extraValues }));
|
|
306
|
-
await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
371
|
+
const compoundResponse = await dsmWebApi(options.dsmOrigin, options.cookieHeader, {
|
|
307
372
|
api: "SYNO.Entry.Request",
|
|
308
373
|
version: "1",
|
|
309
374
|
method: "request",
|
|
@@ -311,7 +376,8 @@ async function installCatalogViaUpload(options, entry, sid, installed) {
|
|
|
311
376
|
mode: jsonApiParam("sequential"),
|
|
312
377
|
compound,
|
|
313
378
|
_sid: sid,
|
|
314
|
-
});
|
|
379
|
+
}, { allowFailure: true });
|
|
380
|
+
assertDsmCompoundResponse(compoundResponse);
|
|
315
381
|
}
|
|
316
382
|
export async function installPackageFromCatalog(options) {
|
|
317
383
|
const entry = options.expectedVersion
|
package/dist/host/credentials.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
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
|
+
}
|
|
4
30
|
export function resolveHostCredentials(hostName) {
|
|
5
31
|
const settingsPath = join(homedir(), ".synoagentcli", "setting.json");
|
|
6
32
|
let settings;
|
|
@@ -8,10 +34,9 @@ export function resolveHostCredentials(hostName) {
|
|
|
8
34
|
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
9
35
|
}
|
|
10
36
|
catch {
|
|
11
|
-
return fromEnv();
|
|
37
|
+
return fromEnv(hostName);
|
|
12
38
|
}
|
|
13
|
-
const
|
|
14
|
-
const profile = settings.hosts?.[selected] ?? settings.hosts?.[settings.defaultHost ?? "default"];
|
|
39
|
+
const profile = findProfile(settings, hostName);
|
|
15
40
|
const username = profile?.username ?? profile?.auth?.username;
|
|
16
41
|
const password = profile?.password ?? profile?.auth?.password;
|
|
17
42
|
if (profile?.url && username && password) {
|
|
@@ -24,10 +49,15 @@ export function resolveHostCredentials(hostName) {
|
|
|
24
49
|
if (profile?.url) {
|
|
25
50
|
return fromEnv(profile.url);
|
|
26
51
|
}
|
|
27
|
-
return fromEnv();
|
|
52
|
+
return fromEnv(hostName);
|
|
28
53
|
}
|
|
29
54
|
function fromEnv(urlOverride) {
|
|
30
|
-
|
|
55
|
+
let dsmOrigin = process.env.DSM_URL;
|
|
56
|
+
if (urlOverride) {
|
|
57
|
+
dsmOrigin = urlOverride.startsWith("http://") || urlOverride.startsWith("https://")
|
|
58
|
+
? urlOverride
|
|
59
|
+
: `http://${urlOverride}:5000`;
|
|
60
|
+
}
|
|
31
61
|
const password = process.env.DSM_PASSWORD;
|
|
32
62
|
const username = process.env.DSM_USERNAME ?? "admin";
|
|
33
63
|
if (!dsmOrigin || !password) {
|
|
@@ -4,6 +4,17 @@ export interface CreateNasHostOptions {
|
|
|
4
4
|
credentials?: HostCredentials;
|
|
5
5
|
}
|
|
6
6
|
export declare function createNasHost(options?: CreateNasHostOptions): NasHost | null;
|
|
7
|
+
declare function synoagentcli(args: string[], profileName?: string): {
|
|
8
|
+
status: number | null;
|
|
9
|
+
stdout: string;
|
|
10
|
+
stderr: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* DSM's User Home read reports the selected volume, not the final share path.
|
|
14
|
+
* Keep the projection strict: an unexpected response must not become a host
|
|
15
|
+
* bind to an arbitrary path.
|
|
16
|
+
*/
|
|
17
|
+
export declare function homesPathFromDsmUserHomeRead(result: Pick<ReturnType<typeof synoagentcli>, "status" | "stdout">): string | undefined;
|
|
7
18
|
/**
|
|
8
19
|
* DSM reports the CPU vendor, not the ISA. Every x86_64 platform Synology
|
|
9
20
|
* ships is Intel or AMD; every aarch64 one is Realtek, Marvell, Annapurna or
|
|
@@ -11,3 +22,4 @@ export declare function createNasHost(options?: CreateNasHostOptions): NasHost |
|
|
|
11
22
|
* `--arch` rather than guessing wrong and building an unusable SPK.
|
|
12
23
|
*/
|
|
13
24
|
export declare function archFamilyForCpuVendor(vendor: string | undefined): "x86_64" | "aarch64" | undefined;
|
|
25
|
+
export {};
|
package/dist/host/dsm-client.js
CHANGED
|
@@ -6,7 +6,7 @@ import { IDENTITY_CGI_PATH } from "./broker-paths.js";
|
|
|
6
6
|
import { packageSourceOrigin } from "../deploy/catalog-publish.js";
|
|
7
7
|
import { lookupSharePathWithSynoagentcli } from "./share-lookup.js";
|
|
8
8
|
import { dsmFetch } from "./dsm-http.js";
|
|
9
|
-
import { installPackageFromCatalog, installPackageFromLocalSpk, fetchInstalledPackageState } from "./catalog-install.js";
|
|
9
|
+
import { installPackageFromCatalog, installPackageFromLocalSpk, fetchInstalledPackageState, packageStateFromRecord, } from "./catalog-install.js";
|
|
10
10
|
import { resolveHostCredentials } from "./credentials.js";
|
|
11
11
|
export function createNasHost(options = {}) {
|
|
12
12
|
const credentials = options.credentials ?? resolveHostCredentials(options.host);
|
|
@@ -27,6 +27,34 @@ function synoagentcli(args, profileName) {
|
|
|
27
27
|
stderr: result.stderr ?? "",
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* DSM's User Home read reports the selected volume, not the final share path.
|
|
32
|
+
* Keep the projection strict: an unexpected response must not become a host
|
|
33
|
+
* bind to an arbitrary path.
|
|
34
|
+
*/
|
|
35
|
+
export function homesPathFromDsmUserHomeRead(result) {
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
let payload;
|
|
40
|
+
try {
|
|
41
|
+
payload = JSON.parse(result.stdout);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (payload.success !== true || payload.data?.enable !== true) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (typeof payload.data.location !== "string") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const volume = payload.data.location.trim().replace(/\/+$/, "");
|
|
53
|
+
if (!/^\/volume[1-9][0-9]*$/.test(volume)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return `${volume}/homes`;
|
|
57
|
+
}
|
|
30
58
|
function createDsmNasHost(credentials, profileName) {
|
|
31
59
|
const dsmOrigin = credentials.dsmOrigin.replace(/\/$/, "");
|
|
32
60
|
const brokerBaseUrl = `${dsmOrigin}${IDENTITY_CGI_PATH}`;
|
|
@@ -184,6 +212,15 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
184
212
|
async resolveSharePath(logicalName) {
|
|
185
213
|
return lookupSharePathWithSynoagentcli(logicalName, profileName);
|
|
186
214
|
},
|
|
215
|
+
async resolveHomesPath() {
|
|
216
|
+
try {
|
|
217
|
+
const result = synoagentcli(["dsm", "control-panel", "user-group", "advanced", "get", "--section", "home"], profileName);
|
|
218
|
+
return homesPathFromDsmUserHomeRead(result);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
187
224
|
async getInstalledProjectEnv(packageIdentity) {
|
|
188
225
|
const cached = projectEnvCache.get(packageIdentity);
|
|
189
226
|
if (cached) {
|
|
@@ -222,6 +259,24 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
222
259
|
throw error;
|
|
223
260
|
}
|
|
224
261
|
},
|
|
262
|
+
async getInstalledPackageState(packageIdentity) {
|
|
263
|
+
await ensureLogin();
|
|
264
|
+
const viaDsm = await fetchInstalledPackageState(dsmOrigin, cookieHeader ?? "", packageIdentity);
|
|
265
|
+
if (viaDsm) {
|
|
266
|
+
return viaDsm;
|
|
267
|
+
}
|
|
268
|
+
const result = synoagentcli(["dsm", "package-center", "get", packageIdentity], profileName);
|
|
269
|
+
if (result.status !== 0) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(result.stdout);
|
|
274
|
+
return packageStateFromRecord(parsed.data);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
225
280
|
async getInstalledPackageVersion(packageIdentity) {
|
|
226
281
|
await ensureLogin();
|
|
227
282
|
const viaDsm = await fetchInstalledPackageState(dsmOrigin, cookieHeader ?? "", packageIdentity);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface DsmCompoundResult {
|
|
2
|
+
api: string;
|
|
3
|
+
method: string;
|
|
4
|
+
success: boolean;
|
|
5
|
+
errorCode?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DsmCompoundResponse {
|
|
8
|
+
hasFail: boolean;
|
|
9
|
+
results: DsmCompoundResult[];
|
|
10
|
+
}
|
|
11
|
+
export type DsmCompoundErrorCode = "DSM_DEPENDENCY_CONFLICT" | "DSM_COMPOUND_FAILED" | "DSM_REQUEST_FAILED";
|
|
12
|
+
export interface DsmCompoundErrorOptions {
|
|
13
|
+
api?: string;
|
|
14
|
+
method?: string;
|
|
15
|
+
errorCode?: number;
|
|
16
|
+
dsmErrorCode?: number;
|
|
17
|
+
detail?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export declare class DsmCompoundError extends Error {
|
|
20
|
+
readonly code: DsmCompoundErrorCode;
|
|
21
|
+
constructor(code: DsmCompoundErrorCode, message: string, options?: DsmCompoundErrorOptions);
|
|
22
|
+
readonly api?: string;
|
|
23
|
+
readonly method?: string;
|
|
24
|
+
readonly errorCode?: number;
|
|
25
|
+
readonly dsmErrorCode?: number;
|
|
26
|
+
readonly detail?: unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse the response envelope returned by SYNO.Entry.Request compound calls.
|
|
30
|
+
* A textual DSM error code is intentionally not converted to a number.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseDsmCompoundResponse(raw: unknown): DsmCompoundResponse;
|
|
33
|
+
/**
|
|
34
|
+
* Fail closed on every DSM compound failure signal. The returned parsed value
|
|
35
|
+
* is only reachable when the top-level request and every compound step pass.
|
|
36
|
+
*/
|
|
37
|
+
export declare function assertDsmCompoundResponse(raw: unknown): DsmCompoundResponse;
|