@synopackageland/cli 0.9.5 → 0.9.7
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 +54 -0
- package/assets/licenses/MUSL-COPYRIGHT +193 -0
- package/assets/licenses/RUST-COPYRIGHT-library.html +5556 -0
- package/dist/add.js +27 -27
- package/dist/build-spk/notices.d.ts +8 -1
- package/dist/build-spk/notices.js +13 -4
- package/dist/build-spk/uninstall-wizard.d.ts +9 -0
- package/dist/build-spk/uninstall-wizard.js +10 -3
- package/dist/build-spk/wizard-i18n.d.ts +1 -1
- package/dist/build-spk/wizard-i18n.js +1 -1
- package/dist/build-spk/wizard-messages.d.ts +33 -0
- package/dist/build-spk/wizard-messages.js +33 -0
- package/dist/capability-overlays.d.ts +10 -5
- package/dist/capability-overlays.js +32 -10
- package/dist/cli.js +5 -5
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/plan.js +1 -1
- package/dist/error-registry.js +44 -3
- package/dist/info.js +2 -2
- package/dist/init.js +53 -28
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +2 -1
- package/dist/runtime/executor-digests.json +22 -0
- package/dist/runtime/executor-policy.d.ts +7 -0
- package/dist/runtime/executor-policy.js +5 -3
- package/dist/runtime/sdk-source.d.ts +14 -2
- package/dist/runtime/sdk-source.js +39 -4
- package/dist/standalone/api-permissions.d.ts +4 -2
- package/dist/standalone/api-permissions.js +6 -5
- package/dist/standalone/binary.d.ts +3 -2
- package/dist/standalone/binary.js +17 -5
- package/dist/standalone/build.js +13 -9
- package/dist/standalone/daemon.d.ts +6 -0
- package/dist/standalone/daemon.js +8 -4
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +39 -10
- package/dist/standalone/lifecycle.js +38 -4
- package/dist/standalone/manifest.d.ts +12 -0
- package/dist/standalone/manifest.js +55 -17
- package/dist/standalone/native-layout.d.ts +1 -7
- package/dist/standalone/native-layout.js +4 -97
- package/dist/standalone/root-ops.d.ts +2 -0
- package/dist/standalone/root-ops.js +2 -0
- package/dist/standalone/rust-build.d.ts +35 -0
- package/dist/standalone/rust-build.js +127 -0
- package/dist/standalone/sdk-check.d.ts +5 -3
- package/dist/standalone/sdk-check.js +21 -8
- package/dist/standalone/spk.js +14 -3
- package/dist/standalone/toolkit.d.ts +10 -0
- package/dist/standalone/toolkit.js +101 -0
- package/dist/standalone/uninstall-hook.d.ts +10 -0
- package/dist/standalone/uninstall-hook.js +71 -0
- package/dist/standalone/uninstall.d.ts +53 -0
- package/dist/standalone/uninstall.js +136 -0
- package/dist/types.d.ts +1 -1
- package/dist/yaml-edit.d.ts +6 -0
- package/dist/yaml-edit.js +31 -0
- package/package.json +2 -2
package/dist/init.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { isMap, isScalar, parse, parseDocument
|
|
2
|
+
import { isMap, isScalar, parse, parseDocument } from "yaml";
|
|
3
|
+
import { setTopLevel } from "./yaml-edit.js";
|
|
3
4
|
import { capabilityNames, capabilitySettings } from "./capabilities.js";
|
|
4
5
|
import { MOVE_FROM_STAGING } from './runtime/delegate-contract.js';
|
|
5
6
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { basename, join, relative, resolve } from "node:path";
|
|
7
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
8
|
import { SDK_MODULE, SDK_PACKAGE } from "./runtime/sdk-source.js";
|
|
8
9
|
import { templatesRoot } from "./template-paths.js";
|
|
9
|
-
import { borrowedTarget, capabilityOverlays } from "./capability-overlays.js";
|
|
10
|
+
import { borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
|
|
11
|
+
import { API_PERMISSIONS_FILE } from "./standalone/api-permissions.js";
|
|
10
12
|
import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "./build-spk/dsm-version.js";
|
|
11
13
|
import { readSharedFolder } from "./standalone/shared-folder.js";
|
|
12
14
|
const TEMPLATE_NAMES = [
|
|
13
15
|
"compose",
|
|
14
16
|
"binary",
|
|
15
17
|
"native-go",
|
|
18
|
+
"native-rust",
|
|
16
19
|
];
|
|
20
|
+
/** Templates built from source with the SDK: capabilities, a Vue or plain UI, a shared folder. */
|
|
21
|
+
const NATIVE_TEMPLATES = ["native-go", "native-rust"];
|
|
22
|
+
const isNativeTemplate = (template) => NATIVE_TEMPLATES.includes(template);
|
|
23
|
+
const BACKEND_KIND = { "native-go": "go", "native-rust": "rust" };
|
|
17
24
|
/** Templates whose App runs through the DSM docker-project resource worker. */
|
|
18
25
|
const CONTAINER_TEMPLATES = [
|
|
19
26
|
"compose",
|
|
@@ -38,7 +45,7 @@ export function nativeCapabilitySettings(names) {
|
|
|
38
45
|
return settings;
|
|
39
46
|
}
|
|
40
47
|
/** Templates whose identity is filled in at init: every generated file may carry a placeholder. */
|
|
41
|
-
const IDENTIFIED = [
|
|
48
|
+
const IDENTIFIED = [...NATIVE_TEMPLATES, "binary"];
|
|
42
49
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
43
50
|
/** npm never publishes files named .gitignore, so templates ship them as `gitignore`. */
|
|
44
51
|
const RENAMED = { gitignore: ".gitignore" };
|
|
@@ -67,9 +74,9 @@ function templateFiles(root, dir = root) {
|
|
|
67
74
|
function targetName(file) {
|
|
68
75
|
return RENAMED[file] ?? file;
|
|
69
76
|
}
|
|
70
|
-
/**
|
|
77
|
+
/** Native templates default to the Vue shell; the CLI and the library entry point agree. */
|
|
71
78
|
function resolvedUi(template, options) {
|
|
72
|
-
return template
|
|
79
|
+
return isNativeTemplate(template) ? options.ui ?? "vue" : options.ui;
|
|
73
80
|
}
|
|
74
81
|
/**
|
|
75
82
|
* Copy one template tree into a project. A template directory may carry a local
|
|
@@ -93,9 +100,9 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
93
100
|
throw new Error("[CAPABILITY_COMBINATION_INVALID] delegate 的 install-selected scope 只能在 Vue 面板確認;plain UI 不支援,請改用 --ui vue。");
|
|
94
101
|
if (options.from && template !== "compose")
|
|
95
102
|
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from 只適用於 compose。");
|
|
96
|
-
if (caps.length && template
|
|
103
|
+
if (caps.length && !isNativeTemplate(template))
|
|
97
104
|
throw new Error("[CAPABILITY_UNSUPPORTED] binary/compose 不支援 --with 能力。");
|
|
98
|
-
const localSdk = template
|
|
105
|
+
const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR ? resolve(process.env.SYNOPKGLAND_SDK_DIR) : undefined;
|
|
99
106
|
if (localSdk)
|
|
100
107
|
assertLocalSdkReady(localSdk, resolvedUi(template, options) === "vue");
|
|
101
108
|
const source = join(templatesRoot(), template);
|
|
@@ -112,14 +119,15 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
112
119
|
if (rootExample && minDsm !== undefined && !isDsmAtLeast(minDsm, DEFAULT_MIN_DSM)) {
|
|
113
120
|
throw new Error(`[MIN_DSM_INVALID] root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
|
|
114
121
|
}
|
|
115
|
-
const capability = capabilityOverlays(caps);
|
|
116
|
-
const
|
|
122
|
+
const capability = isNativeTemplate(template) ? capabilityOverlays(template, caps) : { overlays: [], borrowed: [], removed: [] };
|
|
123
|
+
const borrowed = borrowedFiles(templatesRoot(), capability.borrowed);
|
|
124
|
+
const overlays = [...(ui === "vue" && isNativeTemplate(template) ? VUE_OVERLAYS[template] : []), ...capability.overlays].map(name => join(templatesRoot(), name));
|
|
117
125
|
// Shared legal resources also live in the CLI package; only the template
|
|
118
126
|
// terms are an unconditional part of a generated project.
|
|
119
127
|
const shared = join(templatesRoot(), "_shared");
|
|
120
128
|
const sharedFiles = ["LICENSES/TEMPLATE.txt", "AGENTS.md", "CLAUDE.md"];
|
|
121
129
|
const files = [...templateFiles(source), ...sharedFiles, ...overlays.flatMap(root => templateFiles(root)),
|
|
122
|
-
...
|
|
130
|
+
...borrowed.map(borrowedTarget), ...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : [])];
|
|
123
131
|
const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
|
|
124
132
|
if (collisions.length > 0) {
|
|
125
133
|
throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} 已有 ${collisions.join(", ")};init 不會覆蓋既有檔案。請改用空目錄。`);
|
|
@@ -134,12 +142,14 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
134
142
|
for (const dir of (ui === "vue" ? ["frontend"] : [])) {
|
|
135
143
|
cpSync(join(shared, "third-party-notices.mjs"), join(targetDir, dir, "third-party-notices.mjs"));
|
|
136
144
|
}
|
|
137
|
-
for (const path of
|
|
145
|
+
for (const path of borrowed) {
|
|
146
|
+
mkdirSync(dirname(join(targetDir, borrowedTarget(path))), { recursive: true });
|
|
138
147
|
cpSync(join(templatesRoot(), path), join(targetDir, borrowedTarget(path)));
|
|
148
|
+
}
|
|
139
149
|
for (const file of capability.removed)
|
|
140
150
|
rmSync(join(targetDir, file));
|
|
141
|
-
if (caps.includes("delegate")) {
|
|
142
|
-
const path = join(targetDir, "backend
|
|
151
|
+
if (caps.includes("delegate") && isNativeTemplate(template)) {
|
|
152
|
+
const path = join(targetDir, "backend", API_PERMISSIONS_FILE[BACKEND_KIND[template]]);
|
|
143
153
|
const policy = JSON.parse(readFileSync(path, "utf8"));
|
|
144
154
|
policy.methods["app.status"] = "authenticated";
|
|
145
155
|
writeFileSync(path, JSON.stringify(policy, null, 2) + "\n");
|
|
@@ -151,8 +161,8 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
151
161
|
}
|
|
152
162
|
for (const file of files.map(targetName))
|
|
153
163
|
substitute(join(targetDir, file), substitutions);
|
|
154
|
-
if (localSdk)
|
|
155
|
-
useLocalSdk(targetDir, localSdk);
|
|
164
|
+
if (localSdk && isNativeTemplate(template))
|
|
165
|
+
useLocalSdk(targetDir, localSdk, BACKEND_KIND[template]);
|
|
156
166
|
if (minDsm) {
|
|
157
167
|
const manifest = join(targetDir, template !== "compose" ? "dsm-package.yaml" : "synology-app.yaml");
|
|
158
168
|
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^os_min_ver:.*$/m, `os_min_ver: "${minDsm}"`));
|
|
@@ -165,10 +175,9 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
165
175
|
if (ui !== "vue")
|
|
166
176
|
rmSync(join(targetDir, "frontend"), { recursive: true, force: true });
|
|
167
177
|
if (template !== "compose") {
|
|
178
|
+
// Only the capabilities change: the template's comments document the optional fields.
|
|
168
179
|
const path = join(targetDir, "dsm-package.yaml");
|
|
169
|
-
|
|
170
|
-
raw.capabilities = nativeCapabilitySettings(caps);
|
|
171
|
-
writeFileSync(path, stringify(raw));
|
|
180
|
+
writeFileSync(path, setTopLevel(readFileSync(path, "utf8"), { capabilities: nativeCapabilitySettings(caps) }));
|
|
172
181
|
}
|
|
173
182
|
if (template === "compose" && options.from)
|
|
174
183
|
importCompose(options.from, targetDir, options);
|
|
@@ -236,16 +245,32 @@ function assertLocalSdkReady(sdkDir, vue) {
|
|
|
236
245
|
throw new Error(`[SDK_DIR_NOT_READY] ${ts} 尚未建置;前端安裝本機 SDK 時 npm 不會替它 build。請先執行 (cd ${ts} && npm ci && npm run build),SDK 原始碼改動後也要重新 build。`);
|
|
237
246
|
}
|
|
238
247
|
}
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
248
|
+
/**
|
|
249
|
+
* SYNOPKGLAND_SDK_DIR: the project builds against a local sdk checkout instead
|
|
250
|
+
* of the released version. A Rust backend keeps its Cargo files; at build time
|
|
251
|
+
* git reads the SDK repository from the checkout.
|
|
252
|
+
*/
|
|
253
|
+
function useLocalSdk(targetDir, sdkDir, backend) {
|
|
254
|
+
if (backend === "go") {
|
|
255
|
+
const goMod = join(targetDir, "backend/go.mod");
|
|
256
|
+
writeFileSync(goMod, `${readFileSync(goMod, "utf8")}\nreplace ${SDK_MODULE} => ${sdkDir}\n`);
|
|
257
|
+
}
|
|
243
258
|
const frontend = join(targetDir, "frontend/package.json");
|
|
244
259
|
if (!existsSync(frontend))
|
|
245
260
|
return;
|
|
261
|
+
const local = `file:${join(sdkDir, "ts")}`;
|
|
246
262
|
const pkg = JSON.parse(readFileSync(frontend, "utf8"));
|
|
247
|
-
pkg.dependencies[SDK_PACKAGE] =
|
|
263
|
+
pkg.dependencies[SDK_PACKAGE] = local;
|
|
248
264
|
writeFileSync(frontend, JSON.stringify(pkg, null, 2) + "\n");
|
|
265
|
+
// npm installs what the lockfile records; without the released entry it installs the local package.
|
|
266
|
+
const lockPath = join(targetDir, "frontend/package-lock.json");
|
|
267
|
+
if (!existsSync(lockPath))
|
|
268
|
+
return;
|
|
269
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
270
|
+
if (lock.packages?.[""]?.dependencies)
|
|
271
|
+
lock.packages[""].dependencies[SDK_PACKAGE] = local;
|
|
272
|
+
delete lock.packages?.[`node_modules/${SDK_PACKAGE}`];
|
|
273
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
249
274
|
}
|
|
250
275
|
/** Fill the placeholders of one generated file; files without one, or removed by an overlay, stay untouched. */
|
|
251
276
|
function substitute(path, substitutions) {
|
|
@@ -258,11 +283,11 @@ function substitute(path, substitutions) {
|
|
|
258
283
|
}
|
|
259
284
|
function substitutionsFor(template, targetDir, options) {
|
|
260
285
|
const caps = capabilityNames(options.with ?? []);
|
|
261
|
-
if (options.ui !== undefined && template
|
|
262
|
-
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui 只適用於 native-go。");
|
|
286
|
+
if (options.ui !== undefined && !isNativeTemplate(template))
|
|
287
|
+
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui 只適用於 native-go 與 native-rust。");
|
|
263
288
|
if (options.sharedFolder) {
|
|
264
|
-
if (template
|
|
265
|
-
throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder 只適用於 -t native-go。");
|
|
289
|
+
if (!isNativeTemplate(template))
|
|
290
|
+
throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder 只適用於 -t native-go 與 native-rust。");
|
|
266
291
|
sharedFolderYaml(options.sharedFolder);
|
|
267
292
|
}
|
|
268
293
|
if (options.ui !== undefined && !["plain", "vue"].includes(options.ui))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
|
|
2
2
|
import type { CommandRunner } from '../standalone/build.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type StandaloneManifest } from '../standalone/manifest.js';
|
|
4
4
|
import { type ExecutorPolicy } from './executor-policy.js';
|
|
5
5
|
import { type ExecutorPins, type ExecutorTask } from './executor-tasks.js';
|
|
6
6
|
import { type ExecutorDigestTable } from './executor-source.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from '../build-spk/go-toolchain.js';
|
|
4
|
+
import { sdkBackend } from '../standalone/manifest.js';
|
|
4
5
|
import { StandaloneBuildError } from '../standalone/spk.js';
|
|
5
6
|
import { checkCgiBinary } from '../standalone/binary.js';
|
|
6
7
|
import { executorPolicy, executorPolicyText, sha256 } from './executor-policy.js';
|
|
@@ -23,7 +24,7 @@ export async function buildExecutor(m, arch, work, run, options = {}) {
|
|
|
23
24
|
return undefined;
|
|
24
25
|
let sdk;
|
|
25
26
|
try {
|
|
26
|
-
sdk = requiredSdk(m
|
|
27
|
+
sdk = requiredSdk(sdkBackend(m), options.env, options.download);
|
|
27
28
|
}
|
|
28
29
|
catch (error) {
|
|
29
30
|
throw failure(error);
|
|
@@ -75,5 +75,27 @@
|
|
|
75
75
|
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
76
76
|
},
|
|
77
77
|
"goToolchain": "go1.26.9"
|
|
78
|
+
},
|
|
79
|
+
"0.9.7": {
|
|
80
|
+
"x86_64": "d029ca6129e76cdc5b7016378722af166b511fb346fd1002082e611bec0aef4d",
|
|
81
|
+
"aarch64": "ac8018a9846a8aaca6f06323f033a27f7abef6705601efde57ea02565105a48d",
|
|
82
|
+
"armv7": "f7b4ee5fb90c8f2dd2c56ec1eeb7749300ad117a23d9df6da6840d8b2eff83b8",
|
|
83
|
+
"lifecycle": {
|
|
84
|
+
"x86_64": "f2f0a6aa83399688441b111adee3958d7df50e9d8e33939e3951da393f013819",
|
|
85
|
+
"aarch64": "d0f0d42f367093d5c9318afb4719b9ac76b1019574f1db34dd0fcb3aba44945b",
|
|
86
|
+
"armv7": "02da8b7016abfe00b8f18e27ef2d69bcff1ca376b43e17b8c04983ca6a6ef37d"
|
|
87
|
+
},
|
|
88
|
+
"goToolchain": "go1.26.9"
|
|
89
|
+
},
|
|
90
|
+
"0.9.8": {
|
|
91
|
+
"x86_64": "d029ca6129e76cdc5b7016378722af166b511fb346fd1002082e611bec0aef4d",
|
|
92
|
+
"aarch64": "ac8018a9846a8aaca6f06323f033a27f7abef6705601efde57ea02565105a48d",
|
|
93
|
+
"armv7": "f7b4ee5fb90c8f2dd2c56ec1eeb7749300ad117a23d9df6da6840d8b2eff83b8",
|
|
94
|
+
"lifecycle": {
|
|
95
|
+
"x86_64": "f2f0a6aa83399688441b111adee3958d7df50e9d8e33939e3951da393f013819",
|
|
96
|
+
"aarch64": "d0f0d42f367093d5c9318afb4719b9ac76b1019574f1db34dd0fcb3aba44945b",
|
|
97
|
+
"armv7": "02da8b7016abfe00b8f18e27ef2d69bcff1ca376b43e17b8c04983ca6a6ef37d"
|
|
98
|
+
},
|
|
99
|
+
"goToolchain": "go1.26.9"
|
|
78
100
|
}
|
|
79
101
|
}
|
|
@@ -26,6 +26,8 @@ export interface ExecutorPolicy {
|
|
|
26
26
|
operations: string[];
|
|
27
27
|
/** Below DSM 7.2 the executor registers this Application Portal entry; DSM 7.1 refuses web-config for unsigned packages. */
|
|
28
28
|
portal?: PortalEntry;
|
|
29
|
+
/** The root op the executor runs at uninstall while the approval is live; a key of root. */
|
|
30
|
+
purge?: string;
|
|
29
31
|
root: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/** The exact policy file bytes whose sha256 is the policy pin. */
|
|
@@ -42,6 +44,11 @@ export declare function executorPolicy(m: {
|
|
|
42
44
|
portal?: PortalDeclaration;
|
|
43
45
|
os_min_ver?: string;
|
|
44
46
|
displayname?: string;
|
|
47
|
+
uninstall?: {
|
|
48
|
+
purge: {
|
|
49
|
+
root?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
45
52
|
}): ExecutorPolicy;
|
|
46
53
|
/** Inventory rejects malformed extensions even if a caller recomputes every pin. */
|
|
47
54
|
export declare function validExecutorPolicy(value: unknown, pkg: string): value is ExecutorPolicy;
|
|
@@ -44,9 +44,10 @@ export function executorPolicy(m) {
|
|
|
44
44
|
root[op.name] = sha256(op.body);
|
|
45
45
|
}
|
|
46
46
|
const d = m.capabilities.delegate;
|
|
47
|
+
const purge = m.capabilities.rootOps ? m.uninstall?.purge.root : undefined;
|
|
47
48
|
return { v: 1, package: m.package, app: `SYNO.SDS.${m.package}.Instance`, ...(m.entry ? { entry: m.entry } : {}), ...(d ? { scope: d.scope } : {}),
|
|
48
49
|
...(d?.lifetime ? { destinations: [...d.destinations].sort(), lifetime: d.lifetime } : {}),
|
|
49
|
-
operations: [...new Set(operations)].sort(), ...(portal ? { portal } : {}), root };
|
|
50
|
+
operations: [...new Set(operations)].sort(), ...(portal ? { portal } : {}), ...(purge ? { purge } : {}), root };
|
|
50
51
|
}
|
|
51
52
|
/** The entry the executor registers: a portal App whose os_min_ver is below DSM 7.2. */
|
|
52
53
|
function executorPortal(m) {
|
|
@@ -63,11 +64,12 @@ export function validExecutorPolicy(value, pkg) {
|
|
|
63
64
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
64
65
|
return false;
|
|
65
66
|
const p = value;
|
|
66
|
-
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'root'].includes(key))
|
|
67
|
+
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'root'].includes(key))
|
|
67
68
|
|| p.v !== 1 || p.package !== pkg || p.app !== `SYNO.SDS.${pkg}.Instance`
|
|
68
69
|
|| 'entry' in p && p.entry !== 'admin-only' && p.entry !== 'app-privilege'
|
|
69
70
|
|| !Array.isArray(p.operations) || !p.operations.includes('status') || new Set(p.operations).size !== p.operations.length
|
|
70
|
-
|| !p.root || typeof p.root !== 'object' || Array.isArray(p.root)
|
|
71
|
+
|| !p.root || typeof p.root !== 'object' || Array.isArray(p.root)
|
|
72
|
+
|| 'purge' in p && (typeof p.purge !== 'string' || !Object.hasOwn(p.root, p.purge)))
|
|
71
73
|
return false;
|
|
72
74
|
const base = ['status', 'root.run', 'delegate.create', 'delegate.list', 'delegate.revoke'];
|
|
73
75
|
if (p.operations.some(op => ![...base, ...FILE_OPERATIONS, ...FILE_TASK_OPERATIONS, ...DELEGATE_EXTENSION_OPERATIONS, ...PORTAL_OPERATIONS].includes(op)))
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const SDK_PACKAGE = "@synopackageland/sdk";
|
|
2
2
|
export declare const SDK_MODULE = "github.com/synopackageland/syno-pkg-land-sdk";
|
|
3
3
|
export declare const EXECUTOR_MODULE = "github.com/synopackageland/syno-pkg-land-sdk/executor";
|
|
4
|
+
/** The Rust SDK crate, a git dependency on a release tag of the SDK repository. */
|
|
5
|
+
export declare const SDK_CRATE = "syno-pkg-land-sdk";
|
|
6
|
+
export declare const SDK_GIT_URL = "https://github.com/synopackageland/syno-pkg-land-sdk";
|
|
4
7
|
/** Where one SDK version's Go client and executor sources are read from. */
|
|
5
8
|
export interface SdkSource {
|
|
6
9
|
version: string;
|
|
@@ -8,7 +11,16 @@ export interface SdkSource {
|
|
|
8
11
|
executorDir: string;
|
|
9
12
|
local: boolean;
|
|
10
13
|
}
|
|
11
|
-
/**
|
|
14
|
+
/** How each backend declares the SDK. */
|
|
15
|
+
export declare const SDK_DEPENDENCY: Record<SdkBackend['kind'], string>;
|
|
16
|
+
/** The App backend whose dependency files name its SDK version. */
|
|
17
|
+
export interface SdkBackend {
|
|
18
|
+
kind: 'go' | 'rust';
|
|
19
|
+
dir: string;
|
|
20
|
+
}
|
|
21
|
+
/** The SDK version an App requires, read from its backend's dependency files. */
|
|
22
|
+
export declare function appSdkVersion(backend: SdkBackend): string | undefined;
|
|
23
|
+
/** The SDK version a Go backend requires: `require github.com/synopackageland/syno-pkg-land-sdk vX.Y.Z` in backend/go.mod, its only version source. */
|
|
12
24
|
export declare function sdkRequirement(backendDir: string): string | undefined;
|
|
13
25
|
/** The App's own Go module path, the prefix of every `-X` that targets its packages. */
|
|
14
26
|
export declare function goModulePath(backendDir: string): string | undefined;
|
|
@@ -21,7 +33,7 @@ export declare function sdkSource(version: string, env?: NodeJS.ProcessEnv, down
|
|
|
21
33
|
/** The module cache directory of module@version; the cache is read-only, so builds copy it first. */
|
|
22
34
|
export declare function goModDownload(spec: string, env?: NodeJS.ProcessEnv): string;
|
|
23
35
|
/** The SDK an App builds its executor with; delegate and rootOps cannot build without one. */
|
|
24
|
-
export declare function requiredSdk(
|
|
36
|
+
export declare function requiredSdk(backend: SdkBackend, env?: NodeJS.ProcessEnv, download?: (spec: string) => string): SdkSource;
|
|
25
37
|
/**
|
|
26
38
|
* The SDK repositories are private: Go must fetch them with git, not from
|
|
27
39
|
* proxy.golang.org and sum.golang.org. Says which of the two is missing.
|
|
@@ -5,7 +5,42 @@ import { join } from 'node:path';
|
|
|
5
5
|
export const SDK_PACKAGE = '@synopackageland/sdk';
|
|
6
6
|
export const SDK_MODULE = 'github.com/synopackageland/syno-pkg-land-sdk';
|
|
7
7
|
export const EXECUTOR_MODULE = `${SDK_MODULE}/executor`;
|
|
8
|
-
/** The SDK
|
|
8
|
+
/** The Rust SDK crate, a git dependency on a release tag of the SDK repository. */
|
|
9
|
+
export const SDK_CRATE = 'syno-pkg-land-sdk';
|
|
10
|
+
export const SDK_GIT_URL = `https://${SDK_MODULE}`;
|
|
11
|
+
/** How each backend declares the SDK. */
|
|
12
|
+
export const SDK_DEPENDENCY = {
|
|
13
|
+
go: `add \`require ${SDK_MODULE} v<version>\` to backend/go.mod.`,
|
|
14
|
+
rust: `add \`${SDK_CRATE} = { git = "${SDK_GIT_URL}", tag = "v<version>" }\` to backend/Cargo.toml and commit Cargo.lock.`,
|
|
15
|
+
};
|
|
16
|
+
/** The SDK version an App requires, read from its backend's dependency files. */
|
|
17
|
+
export function appSdkVersion(backend) {
|
|
18
|
+
return backend.kind === 'go' ? sdkRequirement(backend.dir) : cargoSdkRequirement(backend.dir);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The SDK release a Rust backend's Cargo.lock pins: the source
|
|
22
|
+
* `git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=vX.Y.Z#<commit>`.
|
|
23
|
+
* A branch, revision, path or registry source is refused, so the executor and
|
|
24
|
+
* the SDK route policy always come from the same release as the crate.
|
|
25
|
+
*/
|
|
26
|
+
function cargoSdkRequirement(backendDir) {
|
|
27
|
+
let lock;
|
|
28
|
+
try {
|
|
29
|
+
lock = readFileSync(join(backendDir, 'Cargo.lock'), 'utf8');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const sdk = lock.split(/^\[\[package\]\]$/m).filter(entry => /^name = "(.*)"$/m.exec(entry)?.[1] === SDK_CRATE);
|
|
35
|
+
if (!sdk.length)
|
|
36
|
+
return undefined;
|
|
37
|
+
const tag = new RegExp(`^source = "git\\+${SDK_GIT_URL.replaceAll('.', '\\.')}\\?tag=v(\\d+\\.\\d+\\.\\d+)#[0-9a-f]{40}"$`, 'm');
|
|
38
|
+
const version = sdk.length === 1 ? tag.exec(sdk[0])?.[1] : undefined;
|
|
39
|
+
if (!version)
|
|
40
|
+
throw new Error(`[SDK_TAG_REQUIRED] backend/Cargo.lock must pin one ${SDK_CRATE} from a release tag: { git = "${SDK_GIT_URL}", tag = "vX.Y.Z" } in Cargo.toml, then cargo update -p ${SDK_CRATE}.`);
|
|
41
|
+
return version;
|
|
42
|
+
}
|
|
43
|
+
/** The SDK version a Go backend requires: `require github.com/synopackageland/syno-pkg-land-sdk vX.Y.Z` in backend/go.mod, its only version source. */
|
|
9
44
|
export function sdkRequirement(backendDir) {
|
|
10
45
|
const module = SDK_MODULE.replaceAll('.', '\\.');
|
|
11
46
|
return new RegExp(`^\\s*(?:require\\s+)?${module}\\s+v(\\S+)`, 'm').exec(goMod(backendDir))?.[1];
|
|
@@ -54,10 +89,10 @@ export function goModDownload(spec, env = process.env) {
|
|
|
54
89
|
return info.Dir;
|
|
55
90
|
}
|
|
56
91
|
/** The SDK an App builds its executor with; delegate and rootOps cannot build without one. */
|
|
57
|
-
export function requiredSdk(
|
|
58
|
-
const version =
|
|
92
|
+
export function requiredSdk(backend, env = process.env, download) {
|
|
93
|
+
const version = appSdkVersion(backend);
|
|
59
94
|
if (!version)
|
|
60
|
-
throw new Error(`[SDK_REQUIRED] capabilities.delegate and rootOps need the SDK:
|
|
95
|
+
throw new Error(`[SDK_REQUIRED] capabilities.delegate and rootOps need the SDK: ${SDK_DEPENDENCY[backend.kind]}`);
|
|
61
96
|
return download ? sdkSource(version, env, download) : sdkSource(version, env);
|
|
62
97
|
}
|
|
63
98
|
/**
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Capabilities } from "./manifest.js";
|
|
2
|
-
|
|
2
|
+
import { type SdkBackend } from "../runtime/sdk-source.js";
|
|
3
|
+
/** The App route policy inside each backend kind's directory. */
|
|
4
|
+
export declare const API_PERMISSIONS_FILE: Record<SdkBackend["kind"], string>;
|
|
3
5
|
type SdkPermissions = Record<string, Record<string, ApiPermission>>;
|
|
4
6
|
export declare const MAX_PERMISSIONS_BYTES = 65536;
|
|
5
7
|
export declare const MAX_PERMISSION_METHODS = 1024;
|
|
@@ -10,7 +12,7 @@ export interface ApiPermissions {
|
|
|
10
12
|
methods: Record<string, ApiPermission>;
|
|
11
13
|
}
|
|
12
14
|
export declare function parseApiPermissions(raw: Uint8Array): ApiPermissions;
|
|
13
|
-
export declare function readApiPermissions(
|
|
15
|
+
export declare function readApiPermissions(backend: SdkBackend, capabilities?: Capabilities): {
|
|
14
16
|
policy: ApiPermissions;
|
|
15
17
|
digest: string;
|
|
16
18
|
sdkDigest: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { appSdkVersion, sdkSource } from "../runtime/sdk-source.js";
|
|
5
5
|
import { parseDocument } from "yaml";
|
|
6
|
-
|
|
6
|
+
/** The App route policy inside each backend kind's directory. */
|
|
7
|
+
export const API_PERMISSIONS_FILE = { go: "internal/api/permissions.json", rust: "permissions.json" };
|
|
7
8
|
/** SDK-owned routes per capability, in the Go module of the SDK version the App requires. */
|
|
8
9
|
const SDK_PERMISSIONS_FILE = "api/sdk-permissions.json";
|
|
9
10
|
export const MAX_PERMISSIONS_BYTES = 65536;
|
|
@@ -32,14 +33,14 @@ export function parseApiPermissions(raw) {
|
|
|
32
33
|
}
|
|
33
34
|
return value;
|
|
34
35
|
}
|
|
35
|
-
export function readApiPermissions(
|
|
36
|
-
const file = join(
|
|
36
|
+
export function readApiPermissions(backend, capabilities = {}) {
|
|
37
|
+
const file = join(backend.dir, API_PERMISSIONS_FILE[backend.kind]);
|
|
37
38
|
const stat = lstatSync(file);
|
|
38
39
|
if (!stat.isFile() || stat.size > MAX_PERMISSIONS_BYTES)
|
|
39
40
|
throw new Error("permissions must be a regular bounded JSON file");
|
|
40
41
|
const raw = readFileSync(file);
|
|
41
42
|
const policy = parseApiPermissions(raw);
|
|
42
|
-
const version =
|
|
43
|
+
const version = appSdkVersion(backend);
|
|
43
44
|
const sdkRaw = version ? readFileSync(join(sdkSource(version).goDir, SDK_PERMISSIONS_FILE)) : Buffer.from("{}");
|
|
44
45
|
const sdk = JSON.parse(sdkRaw.toString("utf8"));
|
|
45
46
|
for (const methods of Object.values(sdk))
|
|
@@ -4,7 +4,8 @@ export declare const ELF_MACHINE: Record<NativeArchFamily, number>;
|
|
|
4
4
|
/**
|
|
5
5
|
* Checks a prebuilt CGI before it is packaged: a regular file inside the
|
|
6
6
|
* project, a little-endian ELF of `arch`'s class and machine, and statically linked
|
|
7
|
-
* (no PT_INTERP), because DSM models ship different libc versions
|
|
7
|
+
* (no PT_INTERP), because DSM models ship different libc versions; only a
|
|
8
|
+
* build that links against a known DSM glibc may name its `interpreter`.
|
|
8
9
|
* Returns undefined when the file is acceptable.
|
|
9
10
|
*/
|
|
10
|
-
export declare function checkCgiBinary(projectRoot: string, file: string, arch: NativeArchFamily): StandaloneError | undefined;
|
|
11
|
+
export declare function checkCgiBinary(projectRoot: string, file: string, arch: NativeArchFamily, interpreter?: string): StandaloneError | undefined;
|
|
@@ -11,10 +11,11 @@ function fail(code, message) {
|
|
|
11
11
|
/**
|
|
12
12
|
* Checks a prebuilt CGI before it is packaged: a regular file inside the
|
|
13
13
|
* project, a little-endian ELF of `arch`'s class and machine, and statically linked
|
|
14
|
-
* (no PT_INTERP), because DSM models ship different libc versions
|
|
14
|
+
* (no PT_INTERP), because DSM models ship different libc versions; only a
|
|
15
|
+
* build that links against a known DSM glibc may name its `interpreter`.
|
|
15
16
|
* Returns undefined when the file is acceptable.
|
|
16
17
|
*/
|
|
17
|
-
export function checkCgiBinary(projectRoot, file, arch) {
|
|
18
|
+
export function checkCgiBinary(projectRoot, file, arch, interpreter) {
|
|
18
19
|
let stat;
|
|
19
20
|
try {
|
|
20
21
|
stat = lstatSync(file);
|
|
@@ -32,7 +33,7 @@ export function checkCgiBinary(projectRoot, file, arch) {
|
|
|
32
33
|
if (stat.size > MAX_BINARY_BYTES) {
|
|
33
34
|
return fail("STANDALONE_BINARY_TOO_LARGE", `${file} 超過 256 MiB。`);
|
|
34
35
|
}
|
|
35
|
-
return checkElf(readHeader(file), file, arch);
|
|
36
|
+
return checkElf(readHeader(file), file, arch, interpreter);
|
|
36
37
|
}
|
|
37
38
|
function readHeader(file) {
|
|
38
39
|
const fd = openSync(file, "r");
|
|
@@ -58,7 +59,7 @@ function readElfHeader(buf) {
|
|
|
58
59
|
}
|
|
59
60
|
return undefined;
|
|
60
61
|
}
|
|
61
|
-
function checkElf(buf, file, arch) {
|
|
62
|
+
function checkElf(buf, file, arch, interpreter) {
|
|
62
63
|
const bits = ELF_CLASS64[arch] ? 64 : 32;
|
|
63
64
|
const notElf = fail("STANDALONE_BINARY_NOT_ELF", `${file} 不是 ${arch} 需要的 ${bits}-bit little-endian Linux ELF 執行檔。`);
|
|
64
65
|
const header = readElfHeader(buf);
|
|
@@ -73,9 +74,20 @@ function checkElf(buf, file, arch) {
|
|
|
73
74
|
return notElf;
|
|
74
75
|
}
|
|
75
76
|
for (let i = 0; i < phnum; i++) {
|
|
76
|
-
|
|
77
|
+
const entry = phoff + i * phentsize;
|
|
78
|
+
if (buf.readUInt32LE(entry) === PT_INTERP && (interpreter === undefined || interpreterOf(buf, entry, header.class64) !== interpreter)) {
|
|
77
79
|
return fail("STANDALONE_BINARY_DYNAMIC", `${file} 是動態連結執行檔。請改用靜態連結(Go:CGO_ENABLED=0;Rust:*-unknown-linux-musl)。`);
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
return undefined;
|
|
81
83
|
}
|
|
84
|
+
/** The path a PT_INTERP program header names, when it lies within the read header bytes. */
|
|
85
|
+
function interpreterOf(buf, entry, class64) {
|
|
86
|
+
if (entry + (class64 ? 40 : 20) > buf.length)
|
|
87
|
+
return undefined;
|
|
88
|
+
const offset = class64 ? Number(buf.readBigUInt64LE(entry + 8)) : buf.readUInt32LE(entry + 4);
|
|
89
|
+
const size = class64 ? Number(buf.readBigUInt64LE(entry + 32)) : buf.readUInt32LE(entry + 16);
|
|
90
|
+
if (size < 1 || offset + size > buf.length || buf[offset + size - 1] !== 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
return buf.toString("latin1", offset, offset + size - 1);
|
|
93
|
+
}
|
package/dist/standalone/build.js
CHANGED
|
@@ -7,6 +7,7 @@ import { goBuildEnv } from "../build-spk/dsm-arch.js";
|
|
|
7
7
|
import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
|
|
8
8
|
import { checkCgiBinary } from "./binary.js";
|
|
9
9
|
import { loadStandaloneManifest } from "./manifest.js";
|
|
10
|
+
import { buildRustBackend } from "./rust-build.js";
|
|
10
11
|
import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
|
|
11
12
|
import { buildDaemonControl } from "./daemon.js";
|
|
12
13
|
import { buildExecutor } from "../runtime/executor-build.js";
|
|
@@ -102,14 +103,9 @@ export async function buildStandalone(options) {
|
|
|
102
103
|
const executor = await buildExecutor(manifest, arch, archWork, run);
|
|
103
104
|
executor?.warnings.forEach(warning => warnings.add(warning));
|
|
104
105
|
const builtManifest = executor ? { ...manifest, executor } : manifest;
|
|
105
|
-
const cgiPath = await backendFor(builtManifest, arch, archWork, run);
|
|
106
|
-
const problem = checkCgiBinary(manifest.backend.kind === "go" ? archWork : manifest.root, cgiPath, arch);
|
|
107
|
-
if (problem) {
|
|
108
|
-
throw new StandaloneBuildError(problem.code, problem.message);
|
|
109
|
-
}
|
|
106
|
+
const { cgi: cgiPath, worker: backgroundWorker } = await backendFor(builtManifest, arch, archWork, run);
|
|
110
107
|
const outputPath = join(stage, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
|
|
111
108
|
const daemonControl = await buildDaemonControl(manifest, arch, archWork, run);
|
|
112
|
-
const backgroundWorker = await buildBackgroundWorker(builtManifest, arch, archWork, run);
|
|
113
109
|
await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor });
|
|
114
110
|
return outputPath;
|
|
115
111
|
}));
|
|
@@ -133,14 +129,22 @@ async function buildUi(manifest, run) {
|
|
|
133
129
|
throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build(${build.command.join(" ")})失敗。${installHint}\n${result.stderr}`);
|
|
134
130
|
}
|
|
135
131
|
}
|
|
132
|
+
/** The checked CGI of one architecture and, with capabilities.jobs, its fixed worker. */
|
|
136
133
|
async function backendFor(manifest, arch, work, run) {
|
|
137
134
|
const backend = manifest.backend;
|
|
138
|
-
if (backend.kind === "
|
|
139
|
-
return
|
|
135
|
+
if (backend.kind === "rust")
|
|
136
|
+
return buildRustBackend(manifest, arch, work, run);
|
|
137
|
+
const cgi = backend.kind === "binary" ? backend.files[arch] : await buildGoCgi(manifest, backend, arch, work, run);
|
|
138
|
+
const problem = checkCgiBinary(backend.kind === "go" ? work : manifest.root, cgi, arch);
|
|
139
|
+
if (problem) {
|
|
140
|
+
throw new StandaloneBuildError(problem.code, problem.message);
|
|
140
141
|
}
|
|
142
|
+
return { cgi, worker: await buildBackgroundWorker(manifest, arch, work, run) };
|
|
143
|
+
}
|
|
144
|
+
async function buildGoCgi(manifest, backend, arch, work, run) {
|
|
141
145
|
// UI build hooks run after validation. Refuse policy changes rather than
|
|
142
146
|
// compiling CGI permissions different from the launcher metadata snapshot.
|
|
143
|
-
if (readApiPermissions(backend
|
|
147
|
+
if (readApiPermissions(backend, manifest.capabilities).digest !== manifest.apiPermissionsDigest || readApiPermissions(backend, manifest.capabilities).sdkDigest !== manifest.apiSDKPermissionsDigest) {
|
|
144
148
|
throw new StandaloneBuildError("STANDALONE_API_PERMISSIONS_CHANGED", "API permissions changed during build; check and rebuild.");
|
|
145
149
|
}
|
|
146
150
|
const module = goModulePath(backend.dir);
|
|
@@ -18,6 +18,12 @@ export interface Daemon {
|
|
|
18
18
|
uninstall: number;
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
/** A fixed argv or env value: only the package's own directories expand. */
|
|
22
|
+
export declare const token: (v: unknown) => v is string;
|
|
23
|
+
/** An application variable that cannot steer the loader, the shell or DSM. */
|
|
24
|
+
export declare const envKey: (k: string) => boolean;
|
|
25
|
+
/** A relative path to a fixed executable, without `.` or `..` parts. */
|
|
26
|
+
export declare const relativeExec: (v: unknown) => v is string;
|
|
21
27
|
export declare function readDaemon(value: unknown, root: string, arches: NativeArchFamily[], fail: (error: StandaloneError) => void): Daemon | undefined;
|
|
22
28
|
export declare function daemonConfig(manifest: StandaloneManifest): string;
|
|
23
29
|
export declare function buildDaemonControl(manifest: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<string | undefined>;
|
|
@@ -6,18 +6,22 @@ import { checkCgiBinary } from "./binary.js";
|
|
|
6
6
|
import { StandaloneBuildError } from "./spk.js";
|
|
7
7
|
const fields = new Set(["exec", "targets", "args", "env", "timeouts", "uninstall_args"]);
|
|
8
8
|
const record = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
9
|
-
|
|
9
|
+
/** A fixed argv or env value: only the package's own directories expand. */
|
|
10
|
+
export const token = (v) => typeof v === "string" && Buffer.byteLength(v) <= 4096 && !v.includes("\0")
|
|
10
11
|
&& !v.replaceAll("${SYNOPKG_PKGDEST}", "").replaceAll("${SYNOPKG_PKGVAR}", "").includes("${");
|
|
11
|
-
|
|
12
|
+
/** An application variable that cannot steer the loader, the shell or DSM. */
|
|
13
|
+
export const envKey = (k) => /^[A-Z][A-Z0-9_]{0,63}$/.test(k)
|
|
12
14
|
&& !/^(PATH|HOME|TMPDIR|ENV|BASH_ENV|SHELLOPTS|BASHOPTS|GODEBUG|GOTRACEBACK|LD_.*|DYLD_.*|SYNOPKG_.*)$/.test(k);
|
|
15
|
+
/** A relative path to a fixed executable, without `.` or `..` parts. */
|
|
16
|
+
export const relativeExec = (v) => typeof v === "string" && v.length <= 200
|
|
17
|
+
&& /^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(v) && !v.split("/").some(p => p === "." || p === "..");
|
|
13
18
|
export function readDaemon(value, root, arches, fail) {
|
|
14
19
|
if (value === undefined)
|
|
15
20
|
return undefined;
|
|
16
21
|
const invalid = () => { fail({ code: "STANDALONE_DAEMON_INVALID", message: "daemon 只接受固定 exec、targets、args、env 與 timeouts;路徑須在專案內,環境不接受 loader/shell/system 變數。" }); return undefined; };
|
|
17
22
|
if (!record(value) || Object.keys(value).some(k => !fields.has(k)))
|
|
18
23
|
return invalid();
|
|
19
|
-
if (
|
|
20
|
-
|| value.exec.split("/").some(p => p === "." || p === "..") || value.exec.length > 200)
|
|
24
|
+
if (!relativeExec(value.exec))
|
|
21
25
|
return invalid();
|
|
22
26
|
if (!record(value.targets) || Object.keys(value.targets).length !== arches.length)
|
|
23
27
|
return invalid();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExecutorInspection } from "../runtime/executor-inspect.js";
|
|
2
2
|
import type { EntryMode } from "./manifest.js";
|
|
3
3
|
import { type UninstallCleanupItem } from "../runtime/uninstall-cleanup.js";
|
|
4
|
-
export type SpkKind = "native-go" | "binary" | "container" | "native-layout";
|
|
4
|
+
export type SpkKind = "native-go" | "native-rust" | "binary" | "container" | "native-layout";
|
|
5
5
|
export interface SpkInspection {
|
|
6
6
|
kind?: SpkKind;
|
|
7
7
|
package?: string;
|