@synopackageland/cli 0.9.7 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -0
- package/README.md +5 -0
- package/dist/add.js +7 -8
- package/dist/build-spk/dsm-arch.js +1 -1
- package/dist/build-spk/dsm-icons.d.ts +8 -1
- package/dist/build-spk/dsm-icons.js +26 -12
- package/dist/build-spk/dsm-version.d.ts +1 -1
- package/dist/build-spk/dsm-version.js +4 -4
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.js +1 -1
- package/dist/build-spk/png.js +16 -16
- package/dist/capabilities.js +1 -1
- package/dist/capability-overlays.d.ts +16 -5
- package/dist/capability-overlays.js +30 -16
- package/dist/cli.js +107 -107
- package/dist/compose/analyze.js +23 -23
- package/dist/compose/privilege.js +12 -12
- package/dist/contract/i18n.js +3 -3
- package/dist/contract/icon.js +2 -2
- package/dist/contract/posix-all-shares.js +2 -2
- package/dist/contract/schema-validate.js +3 -3
- package/dist/contract/validate.js +16 -16
- package/dist/contract/web-launch.d.ts +2 -1
- package/dist/contract/web-launch.js +3 -2
- package/dist/deploy/deploy.js +8 -8
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/identity-shared.js +1 -1
- package/dist/deploy/native-install-values.js +1 -1
- package/dist/deploy/param-values.js +4 -4
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/port-preflight.js +3 -3
- package/dist/deploy/run-as-values.js +2 -2
- package/dist/deploy/share-permission-preflight.js +6 -6
- package/dist/deploy/share-values.js +3 -3
- package/dist/error-registry.js +369 -346
- package/dist/errors.js +1 -1
- package/dist/host/dsm-port-profile.d.ts +1 -1
- package/dist/host/dsm-port-profile.js +1 -1
- package/dist/info.js +11 -11
- package/dist/init.d.ts +2 -0
- package/dist/init.js +85 -38
- package/dist/inspect.js +5 -5
- package/dist/portal/alias.d.ts +1 -1
- package/dist/portal/alias.js +4 -4
- package/dist/runtime/executor-build.js +12 -1
- package/dist/runtime/executor-digests.json +14 -91
- package/dist/runtime/executor-inspect.js +11 -4
- package/dist/runtime/executor-policy.d.ts +9 -2
- package/dist/runtime/executor-policy.js +27 -8
- package/dist/runtime/root-programs.d.ts +47 -0
- package/dist/runtime/root-programs.js +153 -0
- package/dist/runtime/sdk-source.d.ts +5 -4
- package/dist/runtime/sdk-source.js +8 -4
- package/dist/skill.js +14 -14
- package/dist/spk/check-info.js +17 -17
- package/dist/spk/check-metadata.js +18 -18
- package/dist/spk/check-payload.js +10 -10
- package/dist/spk/rules.d.ts +57 -57
- package/dist/spk/rules.js +50 -50
- package/dist/spk/validate-spk.js +5 -5
- package/dist/standalone/api-permissions.js +1 -1
- package/dist/standalone/background.d.ts +3 -1
- package/dist/standalone/background.js +8 -4
- package/dist/standalone/binary.js +7 -7
- package/dist/standalone/bridge-app.d.ts +23 -0
- package/dist/standalone/bridge-app.js +141 -0
- package/dist/standalone/bridge-backend.d.ts +40 -0
- package/dist/standalone/bridge-backend.js +95 -0
- package/dist/standalone/bridge-build.d.ts +21 -0
- package/dist/standalone/bridge-build.js +62 -0
- package/dist/standalone/bridge-json.d.ts +16 -0
- package/dist/standalone/bridge-json.js +59 -0
- package/dist/standalone/bridge-runtime.d.ts +16 -0
- package/dist/standalone/bridge-runtime.js +46 -0
- package/dist/standalone/bridge-rust.d.ts +16 -0
- package/dist/standalone/bridge-rust.js +89 -0
- package/dist/standalone/build.js +18 -14
- package/dist/standalone/daemon.js +6 -6
- package/dist/standalone/dsm-window.d.ts +10 -5
- package/dist/standalone/dsm-window.js +69 -18
- package/dist/standalone/i18n.js +18 -18
- package/dist/standalone/inspect-bridge.d.ts +31 -0
- package/dist/standalone/inspect-bridge.js +111 -0
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +77 -69
- package/dist/standalone/lifecycle.js +22 -3
- package/dist/standalone/manifest.d.ts +17 -9
- package/dist/standalone/manifest.js +143 -65
- package/dist/standalone/portal-window.js +14 -7
- package/dist/standalone/request-timeout.d.ts +9 -0
- package/dist/standalone/request-timeout.js +13 -0
- package/dist/standalone/root-ops.d.ts +23 -2
- package/dist/standalone/root-ops.js +118 -13
- package/dist/standalone/sdk-check.js +4 -4
- package/dist/standalone/shared-folder.js +7 -7
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +30 -11
- package/dist/standalone/uninstall-hook.js +22 -4
- package/dist/standalone/uninstall.js +7 -7
- package/dist/standalone/window-size.d.ts +17 -0
- package/dist/standalone/window-size.js +31 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +13 -13
- package/package.json +2 -2
- package/schema/bridge-manifest.v1.schema.json +86 -0
- package/skills/reference/create.md +76 -2
- package/skills/reference/ui-json.md +2 -1
- package/skills/reference/ui-vue.md +9 -3
- package/skills/reference/wrap.md +1 -1
- package/dist/standalone/rust-build.d.ts +0 -35
- package/dist/standalone/rust-build.js +0 -127
package/dist/errors.js
CHANGED
|
@@ -11,7 +11,7 @@ export function formatError(error) {
|
|
|
11
11
|
const location = error.span.path
|
|
12
12
|
? `${error.span.file}:${error.span.line}:${error.span.column} (${error.span.path})`
|
|
13
13
|
: `${error.span.file}:${error.span.line}:${error.span.column}`;
|
|
14
|
-
return `[${error.code}] ${location}\n${error.message}\
|
|
14
|
+
return `[${error.code}] ${location}\n${error.message}\nSuggestion: ${error.suggestion}`;
|
|
15
15
|
}
|
|
16
16
|
/** The message of an error thrown with a code: the code in brackets, then the message; the code only once. */
|
|
17
17
|
export function codedMessage(code, message) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The precision caveat for the administrator-only fallback. */
|
|
2
|
-
export declare const DSM_PORT_PROFILE_PRECISION_WARNING = "API
|
|
2
|
+
export declare const DSM_PORT_PROFILE_PRECISION_WARNING = "The API version does not read conf/resource; a custom Compose project name may cause a false positive";
|
|
3
3
|
export interface DsmPortRange {
|
|
4
4
|
start: number;
|
|
5
5
|
end: number;
|
|
@@ -2,7 +2,7 @@ import { basename } from "node:path";
|
|
|
2
2
|
import { composeProjectName } from "../build-spk/resource-worker.js";
|
|
3
3
|
import { dsmFetch } from "./dsm-http.js";
|
|
4
4
|
/** The precision caveat for the administrator-only fallback. */
|
|
5
|
-
export const DSM_PORT_PROFILE_PRECISION_WARNING = "API
|
|
5
|
+
export const DSM_PORT_PROFILE_PRECISION_WARNING = "The API version does not read conf/resource; a custom Compose project name may cause a false positive";
|
|
6
6
|
/**
|
|
7
7
|
* parsePortProfileFilename(). In particular, projectName is everything before
|
|
8
8
|
* the final two dash-separated container segments.
|
package/dist/info.js
CHANGED
|
@@ -67,9 +67,9 @@ export function classifyRootTasks(expected, inventory) {
|
|
|
67
67
|
? result("needs-admin-approval", "Approve missing or outdated root tasks in the DSM package UI.")
|
|
68
68
|
: result("ready", "Boot and control tasks match the executor and policy digests. Executor health must be checked in the App window.");
|
|
69
69
|
}
|
|
70
|
-
async function liveRootCapability(m, host) {
|
|
70
|
+
async function liveRootCapability(m, host, arch) {
|
|
71
71
|
try {
|
|
72
|
-
const policyDigest = sha256(executorPolicyText(executorPolicy(m)));
|
|
72
|
+
const policyDigest = sha256(executorPolicyText(executorPolicy(m, arch)));
|
|
73
73
|
const classified = classifyRootTasks(executorTasks({ package: m.package, policyDigest }, requiredSdk(sdkBackend(m)).executorDir), await host.getRootTaskInventory?.(m.package));
|
|
74
74
|
if (classified.state !== "ready")
|
|
75
75
|
return classified;
|
|
@@ -101,14 +101,14 @@ async function rootCapability(m, host, version) {
|
|
|
101
101
|
return blocked("NAS architecture cannot be matched to the local executor SPK artifact.");
|
|
102
102
|
const spk = join(m.root, "dist", `${m.package}-${version}-${arch}.spk`);
|
|
103
103
|
if (!existsSync(spk))
|
|
104
|
-
return liveRootCapability(m, host);
|
|
104
|
+
return liveRootCapability(m, host, arch);
|
|
105
105
|
try {
|
|
106
106
|
const inspected = await inspectSpk(spk);
|
|
107
107
|
if (inspected.findings.length ||
|
|
108
108
|
inspected.package !== m.package ||
|
|
109
109
|
inspected.version !== version)
|
|
110
110
|
return blocked("Executor SPK artifact failed inspection or identity checks.");
|
|
111
|
-
if (!inspected.executor || JSON.stringify(inspected.executor.policy) !== JSON.stringify(executorPolicy(m)))
|
|
111
|
+
if (!inspected.executor || JSON.stringify(inspected.executor.policy) !== JSON.stringify(executorPolicy(m, arch)))
|
|
112
112
|
return blocked("Executor policy differs from source; rebuild and deploy before approval.");
|
|
113
113
|
const inventory = await host.getRootTaskInventory?.(m.package);
|
|
114
114
|
const classified = classifyRootTasks(inspected.executor.tasks, inventory);
|
|
@@ -204,13 +204,13 @@ async function packageState(pkg, host) {
|
|
|
204
204
|
}
|
|
205
205
|
export function formatProjectInfo(info) {
|
|
206
206
|
return [
|
|
207
|
-
`Package Identity
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
info.openUrl ? `Open URL
|
|
211
|
-
info.readiness ? `Readiness
|
|
212
|
-
...info.capabilities.map((cap) =>
|
|
213
|
-
...(info.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed)
|
|
207
|
+
`Package Identity: ${info.packageIdentity}`,
|
|
208
|
+
`Development instance: ${info.developmentInstance ? "yes" : "no"}`,
|
|
209
|
+
`Installed: ${info.installed ? "yes" : "no"}`,
|
|
210
|
+
info.openUrl ? `Open URL: ${info.openUrl}` : "Open URL: (not set)",
|
|
211
|
+
info.readiness ? `Readiness: ${info.readiness}` : "",
|
|
212
|
+
...info.capabilities.map((cap) => `Capability ${cap.name}: ${cap.state} — ${cap.reason}`),
|
|
213
|
+
...(info.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed): ${item.target} — ${item.condition} ${item.action}`),
|
|
214
214
|
]
|
|
215
215
|
.filter(Boolean)
|
|
216
216
|
.join("\n");
|
package/dist/init.d.ts
CHANGED
|
@@ -31,3 +31,5 @@ export interface InitOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function copyTemplateTree(source: string, target: string): void;
|
|
33
33
|
export declare function initFromTemplate(template: TemplateName, targetDir: string, options?: InitOptions): void;
|
|
34
|
+
/** What to run after init in `dir`, for a template built from source; undefined for the others. */
|
|
35
|
+
export declare function initNextSteps(template: TemplateName, dir: string, frontend: boolean, localSdk: boolean): string | undefined;
|
package/dist/init.js
CHANGED
|
@@ -5,9 +5,10 @@ import { capabilityNames, capabilitySettings } from "./capabilities.js";
|
|
|
5
5
|
import { MOVE_FROM_STAGING } from './runtime/delegate-contract.js';
|
|
6
6
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
8
|
-
import { SDK_MODULE, SDK_PACKAGE } from "./runtime/sdk-source.js";
|
|
8
|
+
import { SDK_MODULE, SDK_PACKAGE, sdkSource } from "./runtime/sdk-source.js";
|
|
9
9
|
import { templatesRoot } from "./template-paths.js";
|
|
10
|
-
import { borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
|
|
10
|
+
import { BASE_BORROWED, BRIDGE_TEMPLATES, borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
|
|
11
|
+
import { copyAppTree, copyNodeSdk, NODE_SDK_PACKAGE } from "./standalone/bridge-app.js";
|
|
11
12
|
import { API_PERMISSIONS_FILE } from "./standalone/api-permissions.js";
|
|
12
13
|
import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "./build-spk/dsm-version.js";
|
|
13
14
|
import { readSharedFolder } from "./standalone/shared-folder.js";
|
|
@@ -15,12 +16,17 @@ const TEMPLATE_NAMES = [
|
|
|
15
16
|
"compose",
|
|
16
17
|
"binary",
|
|
17
18
|
"native-go",
|
|
18
|
-
|
|
19
|
+
...BRIDGE_TEMPLATES,
|
|
19
20
|
];
|
|
20
|
-
/** Templates built from source with the SDK: capabilities, a Vue or plain UI
|
|
21
|
-
const NATIVE_TEMPLATES = ["native-go",
|
|
21
|
+
/** Templates built from source with the SDK: capabilities, a Vue or plain UI. */
|
|
22
|
+
const NATIVE_TEMPLATES = ["native-go", ...BRIDGE_TEMPLATES];
|
|
22
23
|
const isNativeTemplate = (template) => NATIVE_TEMPLATES.includes(template);
|
|
23
|
-
const
|
|
24
|
+
const isBridgeTemplate = (template) => BRIDGE_TEMPLATES.includes(template);
|
|
25
|
+
/** Where each native template keeps its API permissions. */
|
|
26
|
+
const PERMISSIONS_FILE = {
|
|
27
|
+
"native-go": `backend/${API_PERMISSIONS_FILE.go}`, "native-python": `backend/app/${API_PERMISSIONS_FILE.python}`,
|
|
28
|
+
"native-node": `backend/app/${API_PERMISSIONS_FILE.node}`, "native-rust": `backend/${API_PERMISSIONS_FILE.rust}`,
|
|
29
|
+
};
|
|
24
30
|
/** Templates whose App runs through the DSM docker-project resource worker. */
|
|
25
31
|
const CONTAINER_TEMPLATES = [
|
|
26
32
|
"compose",
|
|
@@ -92,22 +98,24 @@ function isTemplateBuildArtifact(path) {
|
|
|
92
98
|
}
|
|
93
99
|
export function initFromTemplate(template, targetDir, options = {}) {
|
|
94
100
|
if (!isTemplateName(template))
|
|
95
|
-
throw new Error(`[INIT_TEMPLATE_UNKNOWN]
|
|
96
|
-
const
|
|
101
|
+
throw new Error(`[INIT_TEMPLATE_UNKNOWN] Unknown template: ${template}`);
|
|
102
|
+
const requested = capabilityNames(options.with ?? []);
|
|
103
|
+
const capability = isNativeTemplate(template) ? capabilityOverlays(template, requested) : { overlays: [], borrowed: [], removed: [], permissions: {}, implied: [], methods: [] };
|
|
104
|
+
const caps = capabilityNames([...new Set([...capability.implied ?? [], ...requested])]);
|
|
97
105
|
if (caps.includes("login") && resolvedUi(template, options) === "plain")
|
|
98
|
-
throw new Error("[CAPABILITY_COMBINATION_INVALID] login
|
|
106
|
+
throw new Error("[CAPABILITY_COMBINATION_INVALID] login needs a standalone sign-in page; plain UI does not support it, use --ui vue.");
|
|
99
107
|
if (caps.includes("delegate") && resolvedUi(template, options) === "plain")
|
|
100
|
-
throw new Error("[CAPABILITY_COMBINATION_INVALID] delegate
|
|
108
|
+
throw new Error("[CAPABILITY_COMBINATION_INVALID] the delegate install-selected scope can only be confirmed in a Vue panel; plain UI does not support it, use --ui vue.");
|
|
101
109
|
if (options.from && template !== "compose")
|
|
102
|
-
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from
|
|
110
|
+
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from applies only to compose.");
|
|
103
111
|
if (caps.length && !isNativeTemplate(template))
|
|
104
|
-
throw new Error("[CAPABILITY_UNSUPPORTED] binary
|
|
112
|
+
throw new Error("[CAPABILITY_UNSUPPORTED] binary/compose does not support --with capabilities.");
|
|
105
113
|
const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR ? resolve(process.env.SYNOPKGLAND_SDK_DIR) : undefined;
|
|
106
114
|
if (localSdk)
|
|
107
115
|
assertLocalSdkReady(localSdk, resolvedUi(template, options) === "vue");
|
|
108
116
|
const source = join(templatesRoot(), template);
|
|
109
117
|
if (!existsSync(source)) {
|
|
110
|
-
throw new Error(
|
|
118
|
+
throw new Error(`Template not found: ${template}`);
|
|
111
119
|
}
|
|
112
120
|
const minDsm = options.minDsm === undefined ? undefined : resolveMinDsm(options.minDsm);
|
|
113
121
|
if (minDsm !== undefined && isContainerTemplate(template)) {
|
|
@@ -119,9 +127,11 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
119
127
|
if (rootExample && minDsm !== undefined && !isDsmAtLeast(minDsm, DEFAULT_MIN_DSM)) {
|
|
120
128
|
throw new Error(`[MIN_DSM_INVALID] root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
|
|
121
129
|
}
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
130
|
+
const vue = ui === "vue" && isNativeTemplate(template) ? VUE_OVERLAYS[template] : { overlays: [], borrowed: [] };
|
|
131
|
+
const base = isNativeTemplate(template) ? BASE_BORROWED[template] : [];
|
|
132
|
+
// Later borrowed files replace earlier ones, as later overlays do.
|
|
133
|
+
const borrowed = borrowedFiles(templatesRoot(), [...base, ...vue.borrowed, ...capability.borrowed]);
|
|
134
|
+
const overlays = [...vue.overlays, ...capability.overlays].map(name => join(templatesRoot(), name));
|
|
125
135
|
// Shared legal resources also live in the CLI package; only the template
|
|
126
136
|
// terms are an unconditional part of a generated project.
|
|
127
137
|
const shared = join(templatesRoot(), "_shared");
|
|
@@ -130,7 +140,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
130
140
|
...borrowed.map(borrowedTarget), ...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : [])];
|
|
131
141
|
const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
|
|
132
142
|
if (collisions.length > 0) {
|
|
133
|
-
throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir}
|
|
143
|
+
throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} already has ${collisions.join(", ")}; init does not overwrite existing files. Use an empty directory.`);
|
|
134
144
|
}
|
|
135
145
|
mkdirSync(targetDir, { recursive: true });
|
|
136
146
|
copyTemplateTree(source, targetDir);
|
|
@@ -148,10 +158,10 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
148
158
|
}
|
|
149
159
|
for (const file of capability.removed)
|
|
150
160
|
rmSync(join(targetDir, file));
|
|
151
|
-
if (
|
|
152
|
-
const path = join(targetDir,
|
|
161
|
+
if (Object.keys(capability.permissions).length && isNativeTemplate(template)) {
|
|
162
|
+
const path = join(targetDir, PERMISSIONS_FILE[template]);
|
|
153
163
|
const policy = JSON.parse(readFileSync(path, "utf8"));
|
|
154
|
-
policy.methods
|
|
164
|
+
Object.assign(policy.methods, capability.permissions);
|
|
155
165
|
writeFileSync(path, JSON.stringify(policy, null, 2) + "\n");
|
|
156
166
|
}
|
|
157
167
|
for (const [from, to] of Object.entries(RENAMED)) {
|
|
@@ -162,7 +172,14 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
162
172
|
for (const file of files.map(targetName))
|
|
163
173
|
substitute(join(targetDir, file), substitutions);
|
|
164
174
|
if (localSdk && isNativeTemplate(template))
|
|
165
|
-
useLocalSdk(targetDir, localSdk
|
|
175
|
+
useLocalSdk(targetDir, localSdk);
|
|
176
|
+
if (isBridgeTemplate(template))
|
|
177
|
+
vendorAppSdk(template, targetDir);
|
|
178
|
+
if (capability.methods?.length && template === "native-rust") {
|
|
179
|
+
// A cross-compiled Rust App cannot describe itself: its manifest lists the methods.
|
|
180
|
+
const manifest = join(targetDir, "dsm-package.yaml");
|
|
181
|
+
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^( {2}methods: \[.*)\]$/m, (_, list) => `${list}${capability.methods.map(name => `, ${name}`).join("")}]`));
|
|
182
|
+
}
|
|
166
183
|
if (minDsm) {
|
|
167
184
|
const manifest = join(targetDir, template !== "compose" ? "dsm-package.yaml" : "synology-app.yaml");
|
|
168
185
|
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^os_min_ver:.*$/m, `os_min_ver: "${minDsm}"`));
|
|
@@ -186,7 +203,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
186
203
|
for (const [arch, goarch] of [["x86_64", "amd64"], ["aarch64", "arm64"]]) {
|
|
187
204
|
const result = spawnSync("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", "-s -w -buildid=", "-o", join(targetDir, "prebuilt", `api-${arch}`), "main.go"], { cwd: join(targetDir, "example"), env: { ...process.env, CGO_ENABLED: "0", GOOS: "linux", GOARCH: goarch }, encoding: "utf8" });
|
|
188
205
|
if (result.error || result.status !== 0)
|
|
189
|
-
throw new Error(`[INIT_BINARY_BUILD_FAILED]
|
|
206
|
+
throw new Error(`[INIT_BINARY_BUILD_FAILED] Install Go and run init again, or provide your own static CGI binary. ${result.stderr ?? result.error}`);
|
|
190
207
|
}
|
|
191
208
|
}
|
|
192
209
|
if (options.sharedFolder) {
|
|
@@ -194,16 +211,29 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
194
211
|
writeFileSync(manifest, readFileSync(manifest, "utf8") + sharedFolderYaml(options.sharedFolder));
|
|
195
212
|
}
|
|
196
213
|
}
|
|
214
|
+
/** Each native template's method tests, as its README runs them; Node.js also compiles the backend/app the SPK ships. */
|
|
215
|
+
const BACKEND_TESTS = {
|
|
216
|
+
"native-go": "go test ./...", "native-python": "python3 -m unittest discover -s tests",
|
|
217
|
+
"native-node": "npm ci && npm test && npm run build", "native-rust": "cargo test --locked",
|
|
218
|
+
};
|
|
219
|
+
/** What to run after init in `dir`, for a template built from source; undefined for the others. */
|
|
220
|
+
export function initNextSteps(template, dir, frontend, localSdk) {
|
|
221
|
+
if (!isNativeTemplate(template))
|
|
222
|
+
return undefined;
|
|
223
|
+
// --install-links packs a local SDK like the released package; a symlink would load the checkout's own node_modules.
|
|
224
|
+
const install = frontend ? ` (cd frontend && npm install${localSdk ? " --install-links" : ""})\n` : "";
|
|
225
|
+
return `Next steps:\n cd ${dir}\n (cd backend && ${BACKEND_TESTS[template]})\n${install} synopkgland build\nSee README.md.`;
|
|
226
|
+
}
|
|
197
227
|
/**
|
|
198
228
|
* --from replaces the example Compose. The manifest takes the requested (or
|
|
199
229
|
* directory-derived) identity and keeps only the example install entries the
|
|
200
|
-
* imported Compose references as `${KEY}` (or `${KEY_UID}
|
|
230
|
+
* imported Compose references as `${KEY}` (or `${KEY_UID}`/`${KEY_GID}`).
|
|
201
231
|
*/
|
|
202
232
|
function importCompose(from, targetDir, options) {
|
|
203
233
|
const imported = readFileSync(from, "utf8");
|
|
204
234
|
const compose = parse(imported);
|
|
205
235
|
if (!compose || typeof compose !== "object" || !compose.services)
|
|
206
|
-
throw new Error("[INIT_COMPOSE_INVALID] --from
|
|
236
|
+
throw new Error("[INIT_COMPOSE_INVALID] --from needs a Compose YAML with services.");
|
|
207
237
|
writeFileSync(join(targetDir, "compose.yaml"), imported);
|
|
208
238
|
const path = join(targetDir, "synology-app.yaml");
|
|
209
239
|
const doc = parseDocument(readFileSync(path, "utf8"));
|
|
@@ -219,7 +249,7 @@ function importCompose(from, targetDir, options) {
|
|
|
219
249
|
}
|
|
220
250
|
function validDisplayName(displayName) {
|
|
221
251
|
if (displayName.trim() === "" || /[\r\n"\\]/.test(displayName)) {
|
|
222
|
-
throw new Error("[INIT_DISPLAY_NAME_INVALID]
|
|
252
|
+
throw new Error("[INIT_DISPLAY_NAME_INVALID] The display name must be single-line and without \" or \\.");
|
|
223
253
|
}
|
|
224
254
|
return displayName;
|
|
225
255
|
}
|
|
@@ -232,29 +262,46 @@ function sharedFolderYaml(options) {
|
|
|
232
262
|
}
|
|
233
263
|
return `\ninstall:\n sharedFolder:\n${Object.entries(folder).map(([k, v]) => ` ${k}: ${v}\n`).join("")}`;
|
|
234
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* The App SDK a bridge App's tests import, in backend/vendor: the Python
|
|
267
|
+
* package, or the Node.js package without what npm would install for its own
|
|
268
|
+
* development. The build ships the release backend.sdk names, from the same
|
|
269
|
+
* source (SYNOPKGLAND_SDK_DIR, or that release's Go module).
|
|
270
|
+
*/
|
|
271
|
+
function vendorAppSdk(template, targetDir) {
|
|
272
|
+
if (template === "native-rust")
|
|
273
|
+
return;
|
|
274
|
+
const version = String(parse(readFileSync(join(targetDir, "dsm-package.yaml"), "utf8")).backend.sdk);
|
|
275
|
+
const sdk = sdkSource(version).goDir;
|
|
276
|
+
const vendor = join(targetDir, "backend/vendor");
|
|
277
|
+
if (template === "native-python")
|
|
278
|
+
return copyAppTree(join(sdk, "python/synopkgland_app"), join(vendor, "synopkgland_app"));
|
|
279
|
+
const target = join(vendor, NODE_SDK_PACKAGE);
|
|
280
|
+
copyNodeSdk(join(sdk, "node"), target);
|
|
281
|
+
const { devDependencies: _dev, scripts: _scripts, ...pkg } = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
|
|
282
|
+
writeFileSync(join(target, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
283
|
+
}
|
|
235
284
|
/**
|
|
236
285
|
* A local sdk checkout must hold the Go module, and for a Vue frontend the
|
|
237
286
|
* built ts package: npm installs a file: package as it is, without its build.
|
|
238
287
|
*/
|
|
239
288
|
function assertLocalSdkReady(sdkDir, vue) {
|
|
240
289
|
if (!existsSync(join(sdkDir, "go.mod"))) {
|
|
241
|
-
throw new Error(`[SDK_DIR_NOT_READY] SYNOPKGLAND_SDK_DIR=${sdkDir}
|
|
290
|
+
throw new Error(`[SDK_DIR_NOT_READY] SYNOPKGLAND_SDK_DIR=${sdkDir} has no go.mod; point it at a syno-pkg-land-sdk checkout.`);
|
|
242
291
|
}
|
|
243
292
|
const ts = join(sdkDir, "ts");
|
|
244
293
|
if (vue && !existsSync(join(ts, "dist"))) {
|
|
245
|
-
throw new Error(`[SDK_DIR_NOT_READY] ${ts}
|
|
294
|
+
throw new Error(`[SDK_DIR_NOT_READY] ${ts} is not built; npm does not build a local SDK when the frontend installs it. Run (cd ${ts} && npm ci && npm run build) first, and rebuild after SDK source changes.`);
|
|
246
295
|
}
|
|
247
296
|
}
|
|
248
297
|
/**
|
|
249
298
|
* SYNOPKGLAND_SDK_DIR: the project builds against a local sdk checkout instead
|
|
250
|
-
* of the released version.
|
|
251
|
-
* git reads the SDK repository from the checkout.
|
|
299
|
+
* of the released version.
|
|
252
300
|
*/
|
|
253
|
-
function useLocalSdk(targetDir, sdkDir
|
|
254
|
-
|
|
255
|
-
|
|
301
|
+
function useLocalSdk(targetDir, sdkDir) {
|
|
302
|
+
const goMod = join(targetDir, "backend/go.mod");
|
|
303
|
+
if (existsSync(goMod))
|
|
256
304
|
writeFileSync(goMod, `${readFileSync(goMod, "utf8")}\nreplace ${SDK_MODULE} => ${sdkDir}\n`);
|
|
257
|
-
}
|
|
258
305
|
const frontend = join(targetDir, "frontend/package.json");
|
|
259
306
|
if (!existsSync(frontend))
|
|
260
307
|
return;
|
|
@@ -284,14 +331,14 @@ function substitute(path, substitutions) {
|
|
|
284
331
|
function substitutionsFor(template, targetDir, options) {
|
|
285
332
|
const caps = capabilityNames(options.with ?? []);
|
|
286
333
|
if (options.ui !== undefined && !isNativeTemplate(template))
|
|
287
|
-
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui
|
|
334
|
+
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui applies only to native-go, native-python, native-node and native-rust.");
|
|
288
335
|
if (options.sharedFolder) {
|
|
289
|
-
if (
|
|
290
|
-
throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder
|
|
336
|
+
if (template !== "native-go")
|
|
337
|
+
throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder applies only to -t native-go.");
|
|
291
338
|
sharedFolderYaml(options.sharedFolder);
|
|
292
339
|
}
|
|
293
340
|
if (options.ui !== undefined && !["plain", "vue"].includes(options.ui))
|
|
294
|
-
throw new Error("[INIT_UI_INVALID] --ui
|
|
341
|
+
throw new Error("[INIT_UI_INVALID] --ui accepts only plain or vue.");
|
|
295
342
|
if (!IDENTIFIED.includes(template)) {
|
|
296
343
|
if (options.from && options.displayName !== undefined)
|
|
297
344
|
validDisplayName(options.displayName);
|
|
@@ -299,7 +346,7 @@ function substitutionsFor(template, targetDir, options) {
|
|
|
299
346
|
}
|
|
300
347
|
const packageId = options.packageId ?? defaultPackageId(targetDir);
|
|
301
348
|
if (!PACKAGE_ID.test(packageId)) {
|
|
302
|
-
throw new Error(`[INIT_PACKAGE_INVALID] package
|
|
349
|
+
throw new Error(`[INIT_PACKAGE_INVALID] package "${packageId}" must start with a letter and contain only alphanumerics (2-64 characters), for example PhotoTagger.`);
|
|
303
350
|
}
|
|
304
351
|
const displayName = validDisplayName(options.displayName ?? defaultDisplayName(targetDir));
|
|
305
352
|
return { __PACKAGE__: packageId, __DISPLAY_NAME__: displayName, __BACKGROUND_ENABLED__: caps.includes("jobs") || caps.includes("delegate") ? "true" : "false" };
|
package/dist/inspect.js
CHANGED
|
@@ -11,7 +11,7 @@ export function inspectProject(project) {
|
|
|
11
11
|
services,
|
|
12
12
|
publishedPorts,
|
|
13
13
|
dataMounts,
|
|
14
|
-
risks: ["
|
|
14
|
+
risks: ["compose.yaml is missing"],
|
|
15
15
|
webHints,
|
|
16
16
|
exposedPorts: [],
|
|
17
17
|
};
|
|
@@ -29,9 +29,9 @@ export function inspectProject(project) {
|
|
|
29
29
|
collectRisks(compose, risks);
|
|
30
30
|
if (publishedPorts.length > 0) {
|
|
31
31
|
const primary = publishedPorts[0];
|
|
32
|
-
webHints.push(
|
|
33
|
-
webHints.push("Web App
|
|
34
|
-
webHints.push("Launch Mode
|
|
32
|
+
webHints.push(`Detected published port ${primary.port}; consider setting adminport: ${primary.port} in synology-app.yaml.`);
|
|
33
|
+
webHints.push("A Web App usually also needs adminprotocol and adminurl.");
|
|
34
|
+
webHints.push("Launch Mode: integration.web.launch is dsm-window (default, DSM window) or new-page (new tab).");
|
|
35
35
|
}
|
|
36
36
|
const exposedPorts = listPortExposures(compose).filter((exposure) => !exposure.loopbackOnly);
|
|
37
37
|
return { services, publishedPorts, dataMounts, risks, webHints, exposedPorts };
|
|
@@ -104,7 +104,7 @@ export function formatInspectReport(report) {
|
|
|
104
104
|
lines.push("Port exposure:");
|
|
105
105
|
lines.push(report.exposedPorts.length > 0
|
|
106
106
|
? report.exposedPorts
|
|
107
|
-
.map((e) => `CONTAINER_PORT_EXPOSED ${e.service}: ${e.entry}
|
|
107
|
+
.map((e) => `CONTAINER_PORT_EXPOSED ${e.service}: ${e.entry} binds to every network interface, so it is reachable without DSM verification; bind it to 127.0.0.1 instead.`)
|
|
108
108
|
.join("\n")
|
|
109
109
|
: "(none)");
|
|
110
110
|
lines.push("");
|
package/dist/portal/alias.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare const RESERVED_PORTAL_ALIASES: readonly string[];
|
|
|
16
16
|
* but leaves nginx serving the alias, and only the root executor purge can
|
|
17
17
|
* reload it. A no-privilege App never runs anything as root.
|
|
18
18
|
*/
|
|
19
|
-
export declare const PORTAL_REQUIRES_EXECUTOR = "portal
|
|
19
|
+
export declare const PORTAL_REQUIRES_EXECUTOR = "portal needs the executor level (capabilities.rootOps or delegate): after uninstall DSM does not reload nginx, and a root executor must remove the sign-in alias. An App without an executor should remove portal and let an administrator configure a reverse proxy in DSM.";
|
|
20
20
|
export interface PortalDeclaration {
|
|
21
21
|
alias: string;
|
|
22
22
|
}
|
package/dist/portal/alias.js
CHANGED
|
@@ -20,7 +20,7 @@ export const RESERVED_PORTAL_ALIASES = [
|
|
|
20
20
|
* but leaves nginx serving the alias, and only the root executor purge can
|
|
21
21
|
* reload it. A no-privilege App never runs anything as root.
|
|
22
22
|
*/
|
|
23
|
-
export const PORTAL_REQUIRES_EXECUTOR = "portal
|
|
23
|
+
export const PORTAL_REQUIRES_EXECUTOR = "portal needs the executor level (capabilities.rootOps or delegate): after uninstall DSM does not reload nginx, and a root executor must remove the sign-in alias. An App without an executor should remove portal and let an administrator configure a reverse proxy in DSM.";
|
|
24
24
|
export function isPortalAlias(value) {
|
|
25
25
|
return typeof value === "string" && PORTAL_ALIAS_PATTERN.test(value) && !RESERVED_PORTAL_ALIASES.includes(value);
|
|
26
26
|
}
|
|
@@ -50,10 +50,10 @@ const ENTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
|
50
50
|
/** Why the executor would refuse this entry; undefined when it accepts it. */
|
|
51
51
|
export function portalEntryProblem(packageId, entry) {
|
|
52
52
|
if (packageId.length > 64 || !ENTRY_PACKAGE.test(packageId))
|
|
53
|
-
return
|
|
53
|
+
return `The package name "${packageId}" cannot be an Application Portal file name: it may contain only alphanumerics separated by . _ -, at most 64 characters.`;
|
|
54
54
|
if (!ENTRY_ALIAS.test(entry.alias))
|
|
55
|
-
return `portal
|
|
55
|
+
return `The portal alias "${entry.alias}" cannot be an Application Portal path.`;
|
|
56
56
|
if (!entry.application || [...entry.application].length > 64 || /\p{Cc}/u.test(entry.application))
|
|
57
|
-
return `displayname
|
|
57
|
+
return `The displayname "${entry.application}" cannot be an Application Portal name: 1-64 characters without control characters.`;
|
|
58
58
|
return undefined;
|
|
59
59
|
}
|
|
@@ -8,6 +8,7 @@ import { executorPolicy, executorPolicyText, sha256 } from './executor-policy.js
|
|
|
8
8
|
import { executorTasks } from './executor-tasks.js';
|
|
9
9
|
import { copyModule, verifyExecutorDigests } from './executor-source.js';
|
|
10
10
|
import { requiredSdk } from './sdk-source.js';
|
|
11
|
+
import { rootProgramsUnsupported } from './root-programs.js';
|
|
11
12
|
/** CLI-owned build flags; the release digest table is computed the same way, so they must not change within an SDK minor. */
|
|
12
13
|
const GO_BUILD = ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o'];
|
|
13
14
|
function failure(error) {
|
|
@@ -63,7 +64,17 @@ export async function buildExecutor(m, arch, work, run, options = {}) {
|
|
|
63
64
|
catch (error) {
|
|
64
65
|
throw failure(error);
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
let policy;
|
|
68
|
+
try {
|
|
69
|
+
policy = executorPolicy(m, arch);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw failure(error);
|
|
73
|
+
}
|
|
74
|
+
const unsupported = rootProgramsUnsupported(policy, sdk.executorDir);
|
|
75
|
+
if (unsupported)
|
|
76
|
+
throw new StandaloneBuildError('EXECUTOR_ROOT_PROGRAMS_UNSUPPORTED', unsupported);
|
|
77
|
+
const policyText = executorPolicyText(policy);
|
|
67
78
|
const pins = { package: m.package, executorDigest, policyDigest: sha256(policyText) };
|
|
68
79
|
return { ...pins, binary, lifecycleBinary, policy, policyText, tasks: executorTasks(pins, sdk.executorDir), implementation: 'executor', sdk: sdk.version, warnings };
|
|
69
80
|
}
|
|
@@ -1,100 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"0.
|
|
3
|
-
"x86_64": "
|
|
4
|
-
"aarch64": "
|
|
5
|
-
"armv7": "
|
|
2
|
+
"0.10.0": {
|
|
3
|
+
"x86_64": "fb2df6e3421361c73a1cd720ec12198e968f0d2a58b18b53151c7781c7f4c1ef",
|
|
4
|
+
"aarch64": "0a0f6d013c0174f3ea54585182ddda14c94064f331189ac4315de813099efcdd",
|
|
5
|
+
"armv7": "3fbb5bc74a03a9767ac06a389a10ff07a353c4c56be6fda7b7555a6759992aba",
|
|
6
6
|
"lifecycle": {
|
|
7
|
-
"x86_64": "
|
|
8
|
-
"aarch64": "
|
|
9
|
-
"armv7": "
|
|
7
|
+
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
8
|
+
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
9
|
+
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
10
10
|
},
|
|
11
11
|
"goToolchain": "go1.26.9"
|
|
12
12
|
},
|
|
13
|
-
"0.
|
|
14
|
-
"x86_64": "
|
|
15
|
-
"aarch64": "
|
|
16
|
-
"armv7": "
|
|
13
|
+
"0.10.1": {
|
|
14
|
+
"x86_64": "51b538e6ec1cb8fa2a4e00291eda097969352d6e2d9a3e7ade5da5359f1065de",
|
|
15
|
+
"aarch64": "9038b5ad7b4822e967f18390ca28b6f0021a3044ed461809b3f7333f17400840",
|
|
16
|
+
"armv7": "22d872cc618e3af4a052955186d36a6851e9f96ad959ee8ea7d3fbb5eec968bd",
|
|
17
17
|
"lifecycle": {
|
|
18
|
-
"x86_64": "
|
|
19
|
-
"aarch64": "
|
|
20
|
-
"armv7": "
|
|
21
|
-
},
|
|
22
|
-
"goToolchain": "go1.26.9"
|
|
23
|
-
},
|
|
24
|
-
"0.9.2": {
|
|
25
|
-
"x86_64": "89c26f18ef05b601963341da6cce43f588bdf2a74039feaf3dfcffdd1295207f",
|
|
26
|
-
"aarch64": "6616953ac1721da64a134898ed1bec93d8e5a81fc044f8bc822751df1a871bdd",
|
|
27
|
-
"armv7": "d9c62b601454b8806a6eeef18a503385b508a8a3b947afa9b433a092979cc6da",
|
|
28
|
-
"lifecycle": {
|
|
29
|
-
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
30
|
-
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
31
|
-
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
32
|
-
},
|
|
33
|
-
"goToolchain": "go1.26.9"
|
|
34
|
-
},
|
|
35
|
-
"0.9.3": {
|
|
36
|
-
"x86_64": "89c26f18ef05b601963341da6cce43f588bdf2a74039feaf3dfcffdd1295207f",
|
|
37
|
-
"aarch64": "6616953ac1721da64a134898ed1bec93d8e5a81fc044f8bc822751df1a871bdd",
|
|
38
|
-
"armv7": "d9c62b601454b8806a6eeef18a503385b508a8a3b947afa9b433a092979cc6da",
|
|
39
|
-
"lifecycle": {
|
|
40
|
-
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
41
|
-
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
42
|
-
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
43
|
-
},
|
|
44
|
-
"goToolchain": "go1.26.9"
|
|
45
|
-
},
|
|
46
|
-
"0.9.4": {
|
|
47
|
-
"x86_64": "fc3c206b6a8e30b7470cedabf6676c027960311469685eab2fe544fe6cc671ac",
|
|
48
|
-
"aarch64": "aa1a8a1b88758ad47ca69d30e9649a24a93b94b5b5fc181ce782035c2c77c497",
|
|
49
|
-
"armv7": "0c39cb98b67735fede6bbbe91471695e26707f377cbc50dd860a1d9a7fe06fcf",
|
|
50
|
-
"lifecycle": {
|
|
51
|
-
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
52
|
-
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
53
|
-
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
54
|
-
},
|
|
55
|
-
"goToolchain": "go1.26.9"
|
|
56
|
-
},
|
|
57
|
-
"0.9.5": {
|
|
58
|
-
"x86_64": "fc3c206b6a8e30b7470cedabf6676c027960311469685eab2fe544fe6cc671ac",
|
|
59
|
-
"aarch64": "aa1a8a1b88758ad47ca69d30e9649a24a93b94b5b5fc181ce782035c2c77c497",
|
|
60
|
-
"armv7": "0c39cb98b67735fede6bbbe91471695e26707f377cbc50dd860a1d9a7fe06fcf",
|
|
61
|
-
"lifecycle": {
|
|
62
|
-
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
63
|
-
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
64
|
-
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
65
|
-
},
|
|
66
|
-
"goToolchain": "go1.26.9"
|
|
67
|
-
},
|
|
68
|
-
"0.9.6": {
|
|
69
|
-
"x86_64": "4241dfebd2b819c2566eca062ee0021b99e7f86bbe4703f50833874f79b3cf54",
|
|
70
|
-
"aarch64": "923514aca8cf6576c6c52c737d53c2c7cc4bfcec40591dcec995005be41add95",
|
|
71
|
-
"armv7": "df959648ac8e6201f1cf9b86b57730c09ba1afb7ecc151ba2f530ad15dd90ccf",
|
|
72
|
-
"lifecycle": {
|
|
73
|
-
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
74
|
-
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
75
|
-
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
76
|
-
},
|
|
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"
|
|
18
|
+
"x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
|
|
19
|
+
"aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
|
|
20
|
+
"armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
|
|
98
21
|
},
|
|
99
22
|
"goToolchain": "go1.26.9"
|
|
100
23
|
}
|
|
@@ -22,10 +22,17 @@ export function inspectExecutor(dir, pkg, arch) {
|
|
|
22
22
|
const policy = JSON.parse(text);
|
|
23
23
|
if (parseDocument(text, { uniqueKeys: true }).errors.length || !validExecutorPolicy(policy, pkg))
|
|
24
24
|
throw new Error('Invalid executor policy.');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const machine = ELF_MACHINE[familyOfArchToken(arch.split(' ')[0]) ?? 'x86_64'];
|
|
26
|
+
for (const [name, entry] of Object.entries(policy.root)) {
|
|
27
|
+
const program = typeof entry !== 'string' && entry.kind === 'binary' ? 'binary' : 'script';
|
|
28
|
+
const path = join(dir, program === 'binary' ? `root/${name}` : `root/${name}.sh`);
|
|
29
|
+
if (!existsSync(path) || !lstatSync(path).isFile())
|
|
30
|
+
throw new Error(`Root program ${name} is missing.`);
|
|
31
|
+
const bytes = readFileSync(path);
|
|
32
|
+
if (sha256(bytes) !== (typeof entry === 'string' ? entry : entry.digest))
|
|
33
|
+
throw new Error(`Root program ${name} digest mismatch.`);
|
|
34
|
+
if (program === 'binary' && (!(lstatSync(path).mode & 0o100) || !bytes.subarray(0, 4).equals(Buffer.from([127, 69, 76, 70])) || bytes.readUInt16LE(18) !== machine))
|
|
35
|
+
throw new Error(`Root program ${name} must be an executable for ${arch}.`);
|
|
29
36
|
}
|
|
30
37
|
const executorDigest = sha256(binary), policyDigest = sha256(text);
|
|
31
38
|
if (typeof plan.sdk !== 'string' || !/^\d+\.\d+\.\d+$/.test(plan.sdk))
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RootOp } from '../standalone/root-ops.js';
|
|
2
2
|
import type { EntryMode } from '../standalone/manifest.js';
|
|
3
3
|
import { type PortalDeclaration, type PortalEntry } from '../portal/alias.js';
|
|
4
|
+
import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
|
|
5
|
+
import { type RootPolicyEntry, type RootSettings } from './root-programs.js';
|
|
4
6
|
export declare const FILE_OPERATIONS: readonly ["files.list", "files.info", "files.createFolder", "files.checkPermission", "files.write", "files.read", "files.rename", "files.copy", "files.move", "files.delete"];
|
|
5
7
|
/** Started File Station tasks are followed with these; they are implied, never declared. */
|
|
6
8
|
export declare const FILE_TASK_OPERATIONS: readonly ["files.taskStatus", "files.taskCancel"];
|
|
@@ -28,7 +30,11 @@ export interface ExecutorPolicy {
|
|
|
28
30
|
portal?: PortalEntry;
|
|
29
31
|
/** The root op the executor runs at uninstall while the approval is live; a key of root. */
|
|
30
32
|
purge?: string;
|
|
31
|
-
root
|
|
33
|
+
/** The root entry the executor runs when the package stops or upgrades. */
|
|
34
|
+
stop?: string;
|
|
35
|
+
root: Record<string, RootPolicyEntry>;
|
|
36
|
+
/** Root programs get a directory only root can read, kept across upgrades. */
|
|
37
|
+
root_state?: true;
|
|
32
38
|
}
|
|
33
39
|
/** The exact policy file bytes whose sha256 is the policy pin. */
|
|
34
40
|
export declare const executorPolicyText: (policy: ExecutorPolicy) => string;
|
|
@@ -41,6 +47,7 @@ export declare function executorPolicy(m: {
|
|
|
41
47
|
rootOps?: true;
|
|
42
48
|
};
|
|
43
49
|
rootOps?: RootOp[];
|
|
50
|
+
rootPrograms?: RootSettings;
|
|
44
51
|
portal?: PortalDeclaration;
|
|
45
52
|
os_min_ver?: string;
|
|
46
53
|
displayname?: string;
|
|
@@ -49,6 +56,6 @@ export declare function executorPolicy(m: {
|
|
|
49
56
|
root?: string;
|
|
50
57
|
};
|
|
51
58
|
};
|
|
52
|
-
}): ExecutorPolicy;
|
|
59
|
+
}, arch?: NativeArchFamily): ExecutorPolicy;
|
|
53
60
|
/** Inventory rejects malformed extensions even if a caller recomputes every pin. */
|
|
54
61
|
export declare function validExecutorPolicy(value: unknown, pkg: string): value is ExecutorPolicy;
|