@synopackageland/cli 0.6.0 → 0.6.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/README.md +12 -1
- package/dist/authorization-sdk.d.ts +7 -0
- package/dist/authorization-sdk.js +7 -0
- package/dist/cli.js +16 -12
- package/dist/error-registry.js +5 -0
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +14 -0
- package/dist/standalone/native-authorization-window.d.ts +2 -0
- package/dist/standalone/native-authorization-window.js +28 -0
- package/dist/standalone/native-layout.d.ts +79 -0
- package/dist/standalone/native-layout.js +345 -0
- package/dist/standalone/native-root-window.d.ts +26 -0
- package/dist/standalone/native-root-window.js +282 -0
- package/dist/templates/native-authorization/AuthorizationGate.vue +89 -0
- package/dist/templates/native-authorization/i18n/de.json +36 -0
- package/dist/templates/native-authorization/i18n/en.json +36 -0
- package/dist/templates/native-authorization/i18n/es.json +36 -0
- package/dist/templates/native-authorization/i18n/fr.json +36 -0
- package/dist/templates/native-authorization/i18n/index.ts +15 -0
- package/dist/templates/native-authorization/i18n/it.json +36 -0
- package/dist/templates/native-authorization/i18n/ja.json +36 -0
- package/dist/templates/native-authorization/i18n/ko.json +36 -0
- package/dist/templates/native-authorization/i18n/zh-CN.json +36 -0
- package/dist/templates/native-authorization/i18n/zh-TW.json +36 -0
- package/dist/validate.js +2 -1
- package/package.json +8 -2
- package/skills/SKILL.md +6 -2
- package/templates/native-authorization/AuthorizationGate.vue +89 -0
- package/templates/native-authorization/i18n/de.json +36 -0
- package/templates/native-authorization/i18n/en.json +36 -0
- package/templates/native-authorization/i18n/es.json +36 -0
- package/templates/native-authorization/i18n/fr.json +36 -0
- package/templates/native-authorization/i18n/index.ts +15 -0
- package/templates/native-authorization/i18n/it.json +36 -0
- package/templates/native-authorization/i18n/ja.json +36 -0
- package/templates/native-authorization/i18n/ko.json +36 -0
- package/templates/native-authorization/i18n/zh-CN.json +36 -0
- package/templates/native-authorization/i18n/zh-TW.json +36 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Native-go needs Go 1.25+ and defaults to Vue; install frontend dependencies befo
|
|
|
28
28
|
|
|
29
29
|
An App session identifies the person but does not create File Station authority. Files without a live DSM cookie return `dsm_session_required`; sensitive business methods may return `session_revalidation_required`. Sign in to DSM on the same hostname. P2 scope is a usage restriction, not the permission boundary; per-operation account/AppPriv checks must reject disabled accounts and denied application access. Digest-locked boot tasks restore the executor after reboot; complete integrated lifecycle acceptance remains pending.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Generated native-go/binary require schema 2 `dsm-package.yaml`, with `backend: {kind: go, dir: backend}` or `backend: {kind: binary, files: {x86_64: prebuilt/api-x86_64}}`. Capability values are `files: session`, `jobs: true` (or `{worker: sha256-v1}`), `identity: standalone`, `rootOps: true`, `delegate: {operations: [files.write], scope: home}` inside `capabilities`. Jobs and daemon are mutually exclusive. Binary/compose do not support files/jobs/login/delegate; current rootOps also requires Go. Business route access comes from `backend/internal/api/permissions.json`; SDK-owned route policy cannot be overridden.
|
|
32
32
|
|
|
33
33
|
See the [delegate guide](../../website/src/content/docs/docs/guides/delegate.md) ([繁體中文](../../website/src/content/docs/zh-tw/docs/guides/delegate.md)) and [administrator lifecycle checks](../../website/src/content/docs/docs/concepts/lifecycle.md) ([繁體中文](../../website/src/content/docs/zh-tw/docs/concepts/lifecycle.md)) for exact operations, errors and cleanup.
|
|
34
34
|
|
|
@@ -94,3 +94,14 @@ Keep `backend/THIRD_PARTY_NOTICES.txt` current if you change Go modules.
|
|
|
94
94
|
The supplied notice inventory covers the pinned template dependencies and Go
|
|
95
95
|
runtime; arbitrary developer binaries, Go module additions, container images,
|
|
96
96
|
custom UI bundles and upstream applications need their own notice inventory.
|
|
97
|
+
|
|
98
|
+
### Existing native products
|
|
99
|
+
|
|
100
|
+
Schema 2 also supports `backend: {kind: native}` with `layout` and `native` declarations.
|
|
101
|
+
This preserves project-owned multi-executable payloads, metadata/conf, lifecycle hooks,
|
|
102
|
+
checksum-pinned DSM toolkits and x86_64/aarch64/armv8/armv7 families. Lifecycle and
|
|
103
|
+
systemd remain package-only; root task authorization is an explicit product responsibility.
|
|
104
|
+
The separate `@synopackageland/cli/authorization-sdk` export provides the pinned-task
|
|
105
|
+
host adapter and Vue authorization gate. Generated Go rootOps/delegate still use the executor.
|
|
106
|
+
See the [native layout and migration guide](../../website/src/content/docs/docs/guides/native-layout.md)
|
|
107
|
+
([繁體中文](../../website/src/content/docs/zh-tw/docs/guides/native-layout.md)).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Explicit build-time adapter for native products with their own pinned root tasks.
|
|
2
|
+
* Generated Go capabilities continue to use the protected executor.
|
|
3
|
+
* No task/grant is created by importing this module.
|
|
4
|
+
*/
|
|
5
|
+
export { ROOT_RUNTIME, type RootTask } from './standalone/native-root-window.js';
|
|
6
|
+
export { AUTHORIZATION_RUNTIME } from './standalone/native-authorization-window.js';
|
|
7
|
+
export declare const authorizationTemplateDirectory: URL;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Explicit build-time adapter for native products with their own pinned root tasks.
|
|
2
|
+
* Generated Go capabilities continue to use the protected executor.
|
|
3
|
+
* No task/grant is created by importing this module.
|
|
4
|
+
*/
|
|
5
|
+
export { ROOT_RUNTIME } from './standalone/native-root-window.js';
|
|
6
|
+
export { AUTHORIZATION_RUNTIME } from './standalone/native-authorization-window.js';
|
|
7
|
+
export const authorizationTemplateDirectory = new URL('../templates/native-authorization/', import.meta.url);
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import { followProjectLogs, followStandaloneLogs } from "./log.js";
|
|
|
23
23
|
import { formatSkillList, readSkill, resolveSkillsRoot } from "./skill.js";
|
|
24
24
|
import { resolveInstallParameterExtraValues } from "./deploy/param-values.js";
|
|
25
25
|
import { findStandaloneRoot, loadStandaloneManifest, } from "./standalone/manifest.js";
|
|
26
|
+
import { buildNativeLayout, isNativeLayout, loadNativeLayout, LAYOUT_ARCHES } from "./standalone/native-layout.js";
|
|
26
27
|
import { buildStandalone } from "./standalone/build.js";
|
|
27
28
|
import { inspectSpk } from "./standalone/inspect-spk.js";
|
|
28
29
|
function failure(code, message, hint = "修正設定後重試;使用 --help 查看指令用法。") {
|
|
@@ -69,12 +70,9 @@ async function standaloneBuild(root, arches, options) {
|
|
|
69
70
|
: options.outDir
|
|
70
71
|
? resolve(options.outDir)
|
|
71
72
|
: undefined;
|
|
72
|
-
const result =
|
|
73
|
-
root,
|
|
74
|
-
arches,
|
|
75
|
-
outDir,
|
|
76
|
-
minDsm: options.minDsm,
|
|
77
|
-
});
|
|
73
|
+
const result = isNativeLayout(root)
|
|
74
|
+
? await buildNativeLayout({ root, arches, outDir, minDsm: options.minDsm })
|
|
75
|
+
: await buildStandalone({ root, arches: arches, outDir, minDsm: options.minDsm });
|
|
78
76
|
return result.outputs.map((output) => {
|
|
79
77
|
if (!options.output)
|
|
80
78
|
return output;
|
|
@@ -141,12 +139,12 @@ export async function runCli(argv) {
|
|
|
141
139
|
throw failure(validation.errors[0].code, formatErrors(validation.errors), validation.errors[0].suggestion);
|
|
142
140
|
const standalone = standaloneRoot(rootOf(command));
|
|
143
141
|
if (standalone) {
|
|
144
|
-
const loaded = loadStandaloneManifest(standalone);
|
|
142
|
+
const loaded = isNativeLayout(standalone) ? loadNativeLayout(standalone) : loadStandaloneManifest(standalone);
|
|
145
143
|
if (!loaded.ok)
|
|
146
144
|
throw failure(loaded.errors[0].code, loaded.errors.map((e) => `[${e.code}] ${e.message}`).join("\n"), "將 dsm-package.yaml 更新為 schema: 2,修正宣告後重試。");
|
|
147
145
|
emit("check", {
|
|
148
146
|
package: loaded.manifest.package,
|
|
149
|
-
capabilities: loaded.manifest.capabilities,
|
|
147
|
+
capabilities: "capabilities" in loaded.manifest ? loaded.manifest.capabilities : {},
|
|
150
148
|
}, "驗證通過。");
|
|
151
149
|
return;
|
|
152
150
|
}
|
|
@@ -377,21 +375,27 @@ export async function runCli(argv) {
|
|
|
377
375
|
.option("--json", "輸出單一 JSON 物件")
|
|
378
376
|
.option("-o, --output <path>", "輸出 SPK,搭配單一 --arch")
|
|
379
377
|
.option("--out-dir <dir>", "輸出目錄,原生專案預設 dist/")
|
|
380
|
-
.option("--arch <families>", `架構,可用逗號分隔:${NATIVE_ARCH_FAMILIES.join(",")}`)
|
|
378
|
+
.option("--arch <families>", `架構,可用逗號分隔:${NATIVE_ARCH_FAMILIES.join(",")};native layout 另支援 armv8,armv7`)
|
|
381
379
|
.option("--min-dsm <version>", "最低 DSM 版本,省略時使用 manifest 值")
|
|
382
380
|
.action(async (options, command) => {
|
|
381
|
+
const root = rootOf(command);
|
|
382
|
+
const standalone = standaloneRoot(root);
|
|
383
383
|
let arches;
|
|
384
384
|
try {
|
|
385
385
|
arches =
|
|
386
386
|
options.arch === undefined
|
|
387
387
|
? undefined
|
|
388
|
-
:
|
|
388
|
+
: standalone && isNativeLayout(standalone)
|
|
389
|
+
? String(options.arch).split(",").map((arch) => {
|
|
390
|
+
if (!Object.hasOwn(LAYOUT_ARCHES, arch))
|
|
391
|
+
throw new Error(`Native layout arch must be one of ${Object.keys(LAYOUT_ARCHES).join(", ")}`);
|
|
392
|
+
return arch;
|
|
393
|
+
})
|
|
394
|
+
: parseArchList(String(options.arch));
|
|
389
395
|
}
|
|
390
396
|
catch (error) {
|
|
391
397
|
throw failure("CLI_ARCH_INVALID", error instanceof Error ? error.message : String(error));
|
|
392
398
|
}
|
|
393
|
-
const root = rootOf(command);
|
|
394
|
-
const standalone = standaloneRoot(root);
|
|
395
399
|
const outputs = standalone
|
|
396
400
|
? await standaloneBuild(standalone, arches, options)
|
|
397
401
|
: await containerBuild(requireProject(root), arches, options);
|
package/dist/error-registry.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export const ERROR_REGISTRY = {
|
|
2
|
+
"NATIVE_LAYOUT_INVALID": {
|
|
3
|
+
code: "NATIVE_LAYOUT_INVALID",
|
|
4
|
+
description: "Native layout manifest, hook, toolkit or artifact violates the package-only contract.",
|
|
5
|
+
suggestion: "Use schema 2 with backend.kind: native; check paths, ELF linkage and checksum pins."
|
|
6
|
+
},
|
|
2
7
|
"COMPOSE_ROUTE_UNSUPPORTED": {
|
|
3
8
|
"code": "COMPOSE_ROUTE_UNSUPPORTED",
|
|
4
9
|
"description": "Compose 沒有可用的 DSM resource worker 執行路徑。",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ExecutorInspection } from "../runtime/executor-inspect.js";
|
|
2
2
|
import { type UninstallCleanupItem } from "../runtime/uninstall-cleanup.js";
|
|
3
|
-
export type SpkKind = "native-go" | "binary" | "container";
|
|
3
|
+
export type SpkKind = "native-go" | "binary" | "container" | "native-layout";
|
|
4
4
|
export interface SpkInspection {
|
|
5
5
|
kind?: SpkKind;
|
|
6
6
|
package?: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inspectNativeLayout } from "./native-layout.js";
|
|
1
2
|
import { inspectExecutor } from "../runtime/executor-inspect.js";
|
|
2
3
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
@@ -27,6 +28,19 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
27
28
|
try {
|
|
28
29
|
await tar.x({ file: spkPath, cwd: dir });
|
|
29
30
|
const findings = [];
|
|
31
|
+
if (!expectedKind && existsSync(join(dir, "native-layout.json"))) {
|
|
32
|
+
const info = readInfo(join(dir, "INFO"), findings, "native-layout");
|
|
33
|
+
const payload = await listPayload(join(dir, "package.tgz"), findings);
|
|
34
|
+
const target = join(dir, "layout-inspection");
|
|
35
|
+
mkdirSync(target);
|
|
36
|
+
await tar.x({ file: join(dir, "package.tgz"), cwd: target });
|
|
37
|
+
findings.push(...inspectNativeLayout(dir, target, info));
|
|
38
|
+
for (const key of ["package", "version", "arch", "os_min_ver", "checksum"])
|
|
39
|
+
if (!info[key])
|
|
40
|
+
findings.push(`Missing INFO ${key}`);
|
|
41
|
+
return { kind: "native-layout", package: info.package, version: info.version, arch: info.arch,
|
|
42
|
+
signed: existsSync(join(dir, "syno_signature.asc")), payload, findings, capabilities: [] };
|
|
43
|
+
}
|
|
30
44
|
const payload = await listPayload(join(dir, "package.tgz"), findings);
|
|
31
45
|
const paths = new Set(payload.map(entry => entry.path));
|
|
32
46
|
let metadata = {};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Shared host flow. CGI plans are never accepted as completed authorization. */
|
|
2
|
+
export declare const AUTHORIZATION_RUNTIME = "function createAuthorizationAccess(options) {\n var busy = null;\n function fail(code) { var error = new Error(code); error.code = code; return error; }\n function status() { return options.call(\"authorization.status\").then(function (d) {\n if (!d || d.command !== \"authorization\" || d.schema_version !== 1 || [\"ready\",\"authorization_required\",\"not_admin\",\"session_expired\",\"unavailable\"].indexOf(d.state) < 0) throw fail(\"authorization_bad_response\");\n return d;\n }); }\n function plan(method) { return options.call(method).then(function (p) {\n if (!p || p.command !== \"authorization\" || p.schema_version !== 1 || p.phase !== \"plan\" || !Array.isArray(p.operations) || p.operations.length === 0 || p.operations.length > options.tasks.length) throw fail(\"authorization_bad_response\");\n var seen = {};\n p.operations.forEach(function (op) { if (seen[op] || !options.tasks.some(function (t) { return t.op === op; })) throw fail(\"authorization_bad_response\"); seen[op] = true; });\n return p;\n }); }\n function exclusive(fn) { if (busy) return busy; busy = Promise.resolve().then(fn).then(function (s) { busy = null; return s; },function (e) { busy = null; throw e; });return busy; }\n function authorize(ops,run) { return ops.reduce(function (previous,op) { return previous.then(function () {\n return options.root.authorize(op).then(function (state) { if (state !== \"ready\") throw fail(\"root_cancelled\"); return run ? options.root.run(op) : null; });\n }); },Promise.resolve()); }\n return {canManage:function () {return !!options.user && options.user().admin === true;},status:status,request:function () { return exclusive(function () {\n return plan(\"authorization.request\").then(function (p) { return authorize(p.operations,p.run === true); }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); },revoke:function () { return exclusive(function () {\n return plan(\"authorization.revoke\").then(function (p) {\n if (p.withdraw === true && typeof options.withdraw !== \"function\") throw fail(\"authorization_bad_response\");\n var disabled = p.withdraw === true ? options.withdraw() : p.disable === true ? authorize([\"helper_disable\"],true) : Promise.resolve();\n return disabled.then(function () { return p.purge === true ? authorize([\"helper_purge\"],true) : null; }).then(function () { return p.operations.reduce(function (previous,op) {return previous.then(function () {return options.root.remove(op);});},Promise.resolve()); });\n }).then(status,function (e) { if (e.code === \"root_cancelled\") return status();throw e; });\n }); }};\n}";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Shared host flow. CGI plans are never accepted as completed authorization. */
|
|
2
|
+
export const AUTHORIZATION_RUNTIME = `function createAuthorizationAccess(options) {
|
|
3
|
+
var busy = null;
|
|
4
|
+
function fail(code) { var error = new Error(code); error.code = code; return error; }
|
|
5
|
+
function status() { return options.call("authorization.status").then(function (d) {
|
|
6
|
+
if (!d || d.command !== "authorization" || d.schema_version !== 1 || ["ready","authorization_required","not_admin","session_expired","unavailable"].indexOf(d.state) < 0) throw fail("authorization_bad_response");
|
|
7
|
+
return d;
|
|
8
|
+
}); }
|
|
9
|
+
function plan(method) { return options.call(method).then(function (p) {
|
|
10
|
+
if (!p || p.command !== "authorization" || p.schema_version !== 1 || p.phase !== "plan" || !Array.isArray(p.operations) || p.operations.length === 0 || p.operations.length > options.tasks.length) throw fail("authorization_bad_response");
|
|
11
|
+
var seen = {};
|
|
12
|
+
p.operations.forEach(function (op) { if (seen[op] || !options.tasks.some(function (t) { return t.op === op; })) throw fail("authorization_bad_response"); seen[op] = true; });
|
|
13
|
+
return p;
|
|
14
|
+
}); }
|
|
15
|
+
function exclusive(fn) { if (busy) return busy; busy = Promise.resolve().then(fn).then(function (s) { busy = null; return s; },function (e) { busy = null; throw e; });return busy; }
|
|
16
|
+
function authorize(ops,run) { return ops.reduce(function (previous,op) { return previous.then(function () {
|
|
17
|
+
return options.root.authorize(op).then(function (state) { if (state !== "ready") throw fail("root_cancelled"); return run ? options.root.run(op) : null; });
|
|
18
|
+
}); },Promise.resolve()); }
|
|
19
|
+
return {canManage:function () {return !!options.user && options.user().admin === true;},status:status,request:function () { return exclusive(function () {
|
|
20
|
+
return plan("authorization.request").then(function (p) { return authorize(p.operations,p.run === true); }).then(status,function (e) { if (e.code === "root_cancelled") return status();throw e; });
|
|
21
|
+
}); },revoke:function () { return exclusive(function () {
|
|
22
|
+
return plan("authorization.revoke").then(function (p) {
|
|
23
|
+
if (p.withdraw === true && typeof options.withdraw !== "function") throw fail("authorization_bad_response");
|
|
24
|
+
var disabled = p.withdraw === true ? options.withdraw() : p.disable === true ? authorize(["helper_disable"],true) : Promise.resolve();
|
|
25
|
+
return disabled.then(function () { return p.purge === true ? authorize(["helper_purge"],true) : null; }).then(function () { return p.operations.reduce(function (previous,op) {return previous.then(function () {return options.root.remove(op);});},Promise.resolve()); });
|
|
26
|
+
}).then(status,function (e) { if (e.code === "root_cancelled") return status();throw e; });
|
|
27
|
+
}); }};
|
|
28
|
+
}`;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CommandRunner } from './build.js';
|
|
2
|
+
export declare const LAYOUT_ARCHES: {
|
|
3
|
+
readonly x86_64: readonly [2, 62];
|
|
4
|
+
readonly aarch64: readonly [2, 183];
|
|
5
|
+
readonly armv8: readonly [2, 183];
|
|
6
|
+
readonly armv7: readonly [1, 40];
|
|
7
|
+
};
|
|
8
|
+
export type LayoutArch = keyof typeof LAYOUT_ARCHES;
|
|
9
|
+
type Hook = {
|
|
10
|
+
command: string[];
|
|
11
|
+
cwd: string;
|
|
12
|
+
env: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
type Toolkit = {
|
|
15
|
+
url: string;
|
|
16
|
+
sha256: string;
|
|
17
|
+
};
|
|
18
|
+
type Binary = {
|
|
19
|
+
path: string;
|
|
20
|
+
linkage: 'static' | 'dynamic';
|
|
21
|
+
arches?: LayoutArch[];
|
|
22
|
+
};
|
|
23
|
+
export interface NativeLayout {
|
|
24
|
+
root: string;
|
|
25
|
+
package: string;
|
|
26
|
+
version: string;
|
|
27
|
+
build: number;
|
|
28
|
+
displayname: string;
|
|
29
|
+
description: string;
|
|
30
|
+
maintainer: string;
|
|
31
|
+
os_min_ver: string;
|
|
32
|
+
privilege: 'none';
|
|
33
|
+
arches: LayoutArch[];
|
|
34
|
+
schema: 2;
|
|
35
|
+
backend: {
|
|
36
|
+
kind: 'native';
|
|
37
|
+
};
|
|
38
|
+
layout: {
|
|
39
|
+
payload: string;
|
|
40
|
+
metadata: string;
|
|
41
|
+
binaries: Binary[];
|
|
42
|
+
};
|
|
43
|
+
native: {
|
|
44
|
+
prepare?: Hook;
|
|
45
|
+
hook: Hook;
|
|
46
|
+
toolkits: Partial<Record<LayoutArch, Toolkit[]>>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export declare function isNativeLayout(root: string): boolean;
|
|
50
|
+
export declare function loadNativeLayout(root: string): {
|
|
51
|
+
ok: true;
|
|
52
|
+
manifest: NativeLayout;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
errors: {
|
|
56
|
+
code: string;
|
|
57
|
+
message: string;
|
|
58
|
+
}[];
|
|
59
|
+
};
|
|
60
|
+
export declare function verifyToolkitArchive(path: string, expected: string): void;
|
|
61
|
+
export declare function layoutFiles(root: string): Array<{
|
|
62
|
+
path: string;
|
|
63
|
+
mode: number;
|
|
64
|
+
sha256: string;
|
|
65
|
+
}>;
|
|
66
|
+
export declare function checkLayoutBinary(root: string, b: Binary, arch: LayoutArch): void;
|
|
67
|
+
export declare function checkLayoutMetadata(root: string, inspection?: boolean, packageId?: string): void;
|
|
68
|
+
export declare function buildNativeLayout(options: {
|
|
69
|
+
root: string;
|
|
70
|
+
arches?: string[];
|
|
71
|
+
outDir?: string;
|
|
72
|
+
build?: number;
|
|
73
|
+
minDsm?: string;
|
|
74
|
+
run?: CommandRunner;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
outputs: string[];
|
|
77
|
+
}>;
|
|
78
|
+
export declare function inspectNativeLayout(dir: string, payload: string, info: Record<string, string>): string[];
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/** Existing native products: the CLI owns archives/INFO, the project owns its layout. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { parse } from 'yaml';
|
|
7
|
+
import { createReproducibleTar } from '../build-spk/reproducible-archive.js';
|
|
8
|
+
import { runCommand } from './build.js';
|
|
9
|
+
import { resolveMinDsm } from '../build-spk/dsm-version.js';
|
|
10
|
+
export const LAYOUT_ARCHES = { x86_64: [2, 62], aarch64: [2, 183], armv8: [2, 183], armv7: [1, 40] };
|
|
11
|
+
function fail(message) { throw Object.assign(new Error(`NATIVE_LAYOUT_INVALID: ${message}`), { code: 'NATIVE_LAYOUT_INVALID' }); }
|
|
12
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
function object(value, fields) {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
15
|
+
fail('expected object');
|
|
16
|
+
const obj = value;
|
|
17
|
+
if (Object.keys(obj).some(k => !fields.includes(k)))
|
|
18
|
+
fail('unknown field');
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
21
|
+
function text(value) {
|
|
22
|
+
if (typeof value !== 'string' || !value || /[\r\n"\\\0]/.test(value))
|
|
23
|
+
fail('invalid text');
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function pathText(value) {
|
|
27
|
+
const path = text(value);
|
|
28
|
+
if (isAbsolute(path) || path.split('/').some(p => p === '..' || p === '') || /[{}]/.test(path.replaceAll('{arch}', '')))
|
|
29
|
+
fail('path must stay inside project');
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
function inside(root, path) {
|
|
33
|
+
const full = resolve(root, path);
|
|
34
|
+
const rel = relative(realpathSync(root), realpathSync(full));
|
|
35
|
+
if (rel === '..' || rel.startsWith('../') || isAbsolute(rel))
|
|
36
|
+
fail('path escapes project');
|
|
37
|
+
return full;
|
|
38
|
+
}
|
|
39
|
+
function hook(value) {
|
|
40
|
+
const h = object(value, ['command', 'cwd', 'env']);
|
|
41
|
+
if (!Array.isArray(h.command) || !h.command.length || !h.command.every((x) => typeof x === 'string' && x.length > 0 && !x.includes('\0')))
|
|
42
|
+
fail('hook requires argv');
|
|
43
|
+
const env = object(h.env ?? {}, Object.keys(h.env ?? {}));
|
|
44
|
+
for (const [key, value] of Object.entries(env))
|
|
45
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key) || typeof value !== 'string' || value.includes('\0') || key.startsWith('SYNOPKGLAND_'))
|
|
46
|
+
fail('invalid hook environment');
|
|
47
|
+
return { command: h.command, cwd: pathText(h.cwd ?? '.'), env };
|
|
48
|
+
}
|
|
49
|
+
export function isNativeLayout(root) {
|
|
50
|
+
try {
|
|
51
|
+
return (() => { const raw = parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')); return raw?.layout !== undefined || raw?.backend?.kind === 'native'; })();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function loadNativeLayout(root) {
|
|
58
|
+
try {
|
|
59
|
+
const m = object(parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')), ['schema', 'backend', 'package', 'version', 'build', 'displayname', 'description', 'maintainer', 'os_min_ver', 'privilege', 'arches', 'layout', 'native']);
|
|
60
|
+
if (m.schema !== 2)
|
|
61
|
+
fail('schema 2 required; migrate schema: 1 to schema: 2 and add backend: {kind: native}');
|
|
62
|
+
const backend = object(m.backend, ['kind']);
|
|
63
|
+
if (backend.kind !== 'native' || (m.privilege !== undefined && m.privilege !== 'none'))
|
|
64
|
+
fail('backend.kind: native and package-only privilege required');
|
|
65
|
+
m.privilege = 'none';
|
|
66
|
+
if (existsSync(join(root, 'compose.yaml')))
|
|
67
|
+
fail('compose.yaml and dsm-package.yaml are mutually exclusive');
|
|
68
|
+
if (!/^[A-Za-z][A-Za-z0-9]{1,63}$/.test(text(m.package)) || !/^\d+(\.\d+){0,3}$/.test(text(m.version)) || !/^7\.\d+(\.\d+)?-\d+$/.test(text(m.os_min_ver)))
|
|
69
|
+
fail('invalid package/version/DSM');
|
|
70
|
+
if (!Number.isSafeInteger(m.build) || m.build < 1 || m.build > 2147483647)
|
|
71
|
+
fail('build must be 1..2147483647');
|
|
72
|
+
for (const k of ['displayname', 'description', 'maintainer'])
|
|
73
|
+
text(m[k]);
|
|
74
|
+
if (!Array.isArray(m.arches) || !m.arches.length || new Set(m.arches).size !== m.arches.length || m.arches.some((a) => !Object.hasOwn(LAYOUT_ARCHES, a)))
|
|
75
|
+
fail('invalid architecture list');
|
|
76
|
+
const layout = object(m.layout, ['payload', 'metadata', 'binaries']);
|
|
77
|
+
layout.payload = pathText(layout.payload);
|
|
78
|
+
layout.metadata = pathText(layout.metadata);
|
|
79
|
+
if (!Array.isArray(layout.binaries) || !layout.binaries.length)
|
|
80
|
+
fail('binary assertions required');
|
|
81
|
+
for (const b of layout.binaries) {
|
|
82
|
+
object(b, ['path', 'linkage', 'arches']);
|
|
83
|
+
pathText(b.path);
|
|
84
|
+
if (!['static', 'dynamic'].includes(b.linkage))
|
|
85
|
+
fail('declare binary linkage');
|
|
86
|
+
if (b.arches !== undefined && (!Array.isArray(b.arches) || !b.arches.length || b.arches.some((a) => !m.arches.includes(a))))
|
|
87
|
+
fail('invalid binary arches');
|
|
88
|
+
}
|
|
89
|
+
const native = object(m.native, ['prepare', 'hook', 'toolkits']);
|
|
90
|
+
const toolkits = object(native.toolkits ?? {}, m.arches);
|
|
91
|
+
for (const entries of Object.values(toolkits)) {
|
|
92
|
+
if (!Array.isArray(entries) || !entries.length)
|
|
93
|
+
fail('toolkits require archives');
|
|
94
|
+
for (const t of entries) {
|
|
95
|
+
object(t, ['url', 'sha256']);
|
|
96
|
+
const u = new URL(t.url);
|
|
97
|
+
if (u.protocol !== 'https:' || u.username || u.password || !/^[a-f0-9]{64}$/.test(t.sha256))
|
|
98
|
+
fail('toolkits require HTTPS and SHA-256');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, manifest: { ...m, root: resolve(root), native: { hook: hook(native.hook), ...(native.prepare ? { prepare: hook(native.prepare) } : {}), toolkits } } };
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return { ok: false, errors: [{ code: 'NATIVE_LAYOUT_INVALID', message: String(error) }] };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function verifyToolkitArchive(path, expected) {
|
|
108
|
+
if (!lstatSync(path).isFile() || digest(readFileSync(path)) !== expected)
|
|
109
|
+
fail('toolkit checksum mismatch');
|
|
110
|
+
}
|
|
111
|
+
async function toolkitFor(m, arch, run) {
|
|
112
|
+
const inputs = m.native.toolkits[arch] ?? [];
|
|
113
|
+
if (!inputs.length)
|
|
114
|
+
return '';
|
|
115
|
+
const cache = resolve(process.env.SYNOPKGLAND_CACHE_DIR ?? join(m.root, '.synopkgland-cache'));
|
|
116
|
+
mkdirSync(cache, { recursive: true, mode: 0o700 });
|
|
117
|
+
if (lstatSync(cache).isSymbolicLink() || (lstatSync(cache).mode & 0o022))
|
|
118
|
+
fail('toolkit cache must be owned and private');
|
|
119
|
+
const archives = [];
|
|
120
|
+
for (const input of inputs) {
|
|
121
|
+
const archive = join(cache, `${input.sha256}.txz`);
|
|
122
|
+
if (!existsSync(archive)) {
|
|
123
|
+
const download = mkdtempSync(join(cache, '.download-'));
|
|
124
|
+
try {
|
|
125
|
+
const out = join(download, 'archive');
|
|
126
|
+
const r = run('curl', ['--fail', '--location', '--proto', '=https', '--proto-redir', '=https', '--retry', '2', '--retry-all-errors', '--connect-timeout', '20', '--max-time', '600', '--output', out, input.url], { cwd: m.root, env: { ...process.env } });
|
|
127
|
+
if (r.status !== 0)
|
|
128
|
+
fail('toolkit download failed');
|
|
129
|
+
verifyToolkitArchive(out, input.sha256);
|
|
130
|
+
renameSync(out, archive);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
rmSync(download, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
verifyToolkitArchive(archive, input.sha256);
|
|
137
|
+
archives.push(archive);
|
|
138
|
+
}
|
|
139
|
+
// Re-extract verified bytes: a package-writable marker never authenticates an extracted tree.
|
|
140
|
+
const stage = mkdtempSync(join(cache, `toolkit-${arch}-`));
|
|
141
|
+
try {
|
|
142
|
+
for (const archive of archives) {
|
|
143
|
+
const r = run('tar', ['-xJf', archive, '-C', stage, '--keep-directory-symlink'], { cwd: m.root, env: { ...process.env } });
|
|
144
|
+
if (r.status !== 0)
|
|
145
|
+
fail('toolkit extraction failed');
|
|
146
|
+
}
|
|
147
|
+
return stage;
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
rmSync(stage, { recursive: true, force: true });
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function runHook(m, h, env, run) {
|
|
155
|
+
const [cmd, ...args] = h.command;
|
|
156
|
+
const r = run(cmd, args, { cwd: inside(m.root, h.cwd), env: { ...process.env, ...h.env, ...env } });
|
|
157
|
+
if (r.status !== 0 || r.missing)
|
|
158
|
+
fail(`native hook failed: ${r.stderr}`);
|
|
159
|
+
}
|
|
160
|
+
export function layoutFiles(root) {
|
|
161
|
+
if (!lstatSync(root).isDirectory() || lstatSync(root).isSymbolicLink())
|
|
162
|
+
fail('layout root must be a regular directory');
|
|
163
|
+
const files = [];
|
|
164
|
+
let total = 0;
|
|
165
|
+
const walk = (prefix) => {
|
|
166
|
+
for (const name of readdirSync(join(root, prefix)).sort()) {
|
|
167
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
168
|
+
const s = lstatSync(join(root, path));
|
|
169
|
+
if (s.isSymbolicLink() || (!s.isFile() && !s.isDirectory()))
|
|
170
|
+
fail('layout contains symlink or special file');
|
|
171
|
+
if (s.mode & 0o6000)
|
|
172
|
+
fail('privileged file modes are forbidden');
|
|
173
|
+
if (s.isDirectory())
|
|
174
|
+
walk(path);
|
|
175
|
+
else {
|
|
176
|
+
total += s.size;
|
|
177
|
+
if (s.size > 256 * 1024 * 1024 || total > 1024 * 1024 * 1024)
|
|
178
|
+
fail('layout exceeds size bound');
|
|
179
|
+
files.push({ path, mode: s.mode & 0o111 ? 0o755 : 0o644, sha256: digest(readFileSync(join(root, path))) });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
walk('');
|
|
184
|
+
return files;
|
|
185
|
+
}
|
|
186
|
+
export function checkLayoutBinary(root, b, arch) {
|
|
187
|
+
const path = inside(root, b.path);
|
|
188
|
+
const bytes = readFileSync(path);
|
|
189
|
+
const [cls, machine] = LAYOUT_ARCHES[arch];
|
|
190
|
+
if (bytes.length < 64 || bytes.toString('latin1', 0, 4) !== '\x7fELF' || bytes[4] !== cls || bytes[5] !== 1 || bytes.readUInt16LE(18) !== machine || ![2, 3].includes(bytes.readUInt16LE(16)))
|
|
191
|
+
fail(`ELF architecture mismatch: ${b.path}`);
|
|
192
|
+
const offset = cls === 1 ? bytes.readUInt32LE(28) : Number(bytes.readBigUInt64LE(32));
|
|
193
|
+
const size = bytes.readUInt16LE(cls === 1 ? 42 : 54);
|
|
194
|
+
const count = bytes.readUInt16LE(cls === 1 ? 44 : 56);
|
|
195
|
+
if (!count || size < (cls === 1 ? 32 : 56) || !Number.isSafeInteger(offset) || offset + size * count > bytes.length)
|
|
196
|
+
fail('invalid ELF program headers');
|
|
197
|
+
let dynamic = false;
|
|
198
|
+
for (let i = 0; i < count; i++)
|
|
199
|
+
if (bytes.readUInt32LE(offset + size * i) === 3)
|
|
200
|
+
dynamic = true;
|
|
201
|
+
if (dynamic !== (b.linkage === 'dynamic'))
|
|
202
|
+
fail(`ELF linkage mismatch: ${b.path}`);
|
|
203
|
+
if (!(lstatSync(path).mode & 0o111))
|
|
204
|
+
fail(`binary must be executable: ${b.path}`);
|
|
205
|
+
}
|
|
206
|
+
function copyTree(from, to) {
|
|
207
|
+
const entries = layoutFiles(from);
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const out = join(to, entry.path);
|
|
210
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
211
|
+
copyFileSync(join(from, entry.path), out);
|
|
212
|
+
chmodSync(out, entry.mode);
|
|
213
|
+
}
|
|
214
|
+
return entries;
|
|
215
|
+
}
|
|
216
|
+
export function checkLayoutMetadata(root, inspection = false, packageId) {
|
|
217
|
+
layoutFiles(root);
|
|
218
|
+
const p = object(JSON.parse(readFileSync(join(root, 'conf/privilege'), 'utf8')), ['defaults', 'username', 'groupname']);
|
|
219
|
+
object(p.defaults, ['run-as']);
|
|
220
|
+
if (p.defaults?.['run-as'] !== 'package' || Object.keys(p).some(k => !['defaults', 'username', 'groupname'].includes(k)) || ['username', 'groupname'].some(k => p[k] !== undefined && p[k] !== packageId) || Object.keys(p.defaults).some(k => k !== 'run-as'))
|
|
221
|
+
fail('native layout requires package-only lifecycle privilege');
|
|
222
|
+
if (!existsSync(join(root, 'scripts/start-stop-status')))
|
|
223
|
+
fail('start-stop-status missing');
|
|
224
|
+
if (existsSync(join(root, 'conf/resource'))) {
|
|
225
|
+
const resource = object(JSON.parse(readFileSync(join(root, 'conf/resource'), 'utf8')), ['systemd-unit']);
|
|
226
|
+
if (resource['systemd-unit'] !== undefined && Object.keys(object(resource['systemd-unit'], [])).length)
|
|
227
|
+
fail('unsupported resource worker');
|
|
228
|
+
}
|
|
229
|
+
if (existsSync(join(root, 'conf/systemd'))) {
|
|
230
|
+
for (const entry of layoutFiles(join(root, 'conf/systemd'))) {
|
|
231
|
+
const unit = readFileSync(join(root, 'conf/systemd', entry.path), 'utf8');
|
|
232
|
+
// Keep the package-only grammar unambiguous; systemd joins continued lines
|
|
233
|
+
// before interpreting Exec prefixes, which a physical-line check misses.
|
|
234
|
+
if (/\\\s*$/m.test(unit))
|
|
235
|
+
fail('systemd unit continuations are unsupported');
|
|
236
|
+
let section = '';
|
|
237
|
+
for (const line of unit.split('\n')) {
|
|
238
|
+
const heading = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
239
|
+
if (heading)
|
|
240
|
+
section = heading[1];
|
|
241
|
+
if (/^\s*(?:User|Group|Exec\w*)\s*=/.test(line) && section !== 'Service')
|
|
242
|
+
fail('systemd execution directives require the package Service section');
|
|
243
|
+
}
|
|
244
|
+
if (!/^[^/]+\.service$/.test(entry.path))
|
|
245
|
+
fail('only package service units are supported');
|
|
246
|
+
const users = [...unit.matchAll(/^\s*User\s*=\s*(.*?)\s*$/gm)].map(m => m[1]);
|
|
247
|
+
const groups = [...unit.matchAll(/^\s*Group\s*=\s*(.*?)\s*$/gm)].map(m => m[1]);
|
|
248
|
+
if (!packageId || users.length !== 1 || users[0] !== packageId || groups.some(group => group !== packageId) || /^\s*(?:AmbientCapabilities|SupplementaryGroups|PermissionsStartOnly|RootDirectory|RootImage)\s*=/m.test(unit) || /^\s*Exec\w*\s*=\s*[-:@|]*[+!]/m.test(unit))
|
|
249
|
+
fail('systemd unit must run only as the package account');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const name of readdirSync(root))
|
|
253
|
+
if (!['conf', 'scripts', 'WIZARD_UIFILES', 'PACKAGE_ICON.PNG', 'PACKAGE_ICON_256.PNG', ...(inspection ? ['INFO', 'package.tgz', 'native-layout.json', 'layout-inspection', 'syno_signature.asc'] : [])].includes(name))
|
|
254
|
+
fail(`unsupported SPK metadata: ${name}`);
|
|
255
|
+
for (const f of layoutFiles(root)) {
|
|
256
|
+
if (f.path.startsWith('scripts/') && /\bsudo\b|\bchown\s+root\b|chmod\s+[ugo]*\+s\b/.test(readFileSync(join(root, f.path), 'utf8')))
|
|
257
|
+
fail('root lifecycle operation forbidden');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export async function buildNativeLayout(options) {
|
|
261
|
+
const loaded = loadNativeLayout(options.root);
|
|
262
|
+
if (!loaded.ok)
|
|
263
|
+
fail(loaded.errors.map(e => e.message).join('\n'));
|
|
264
|
+
const m = loaded.manifest;
|
|
265
|
+
if (options.minDsm !== undefined)
|
|
266
|
+
m.os_min_ver = resolveMinDsm(options.minDsm);
|
|
267
|
+
if (options.build !== undefined) {
|
|
268
|
+
if (!Number.isSafeInteger(options.build) || options.build < 1 || options.build > 2147483647)
|
|
269
|
+
fail('invalid build override');
|
|
270
|
+
m.build = options.build;
|
|
271
|
+
}
|
|
272
|
+
const arches = options.arches ?? m.arches;
|
|
273
|
+
if (!arches.length || new Set(arches).size !== arches.length || arches.some(a => !m.arches.includes(a)))
|
|
274
|
+
fail('architecture not declared');
|
|
275
|
+
const run = options.run ?? runCommand;
|
|
276
|
+
const version = `${m.version}-${String(m.build).padStart(4, '0')}`;
|
|
277
|
+
const env = { SYNOPKGLAND_PACKAGE: m.package, SYNOPKGLAND_VERSION: m.version, SYNOPKGLAND_BUILD: String(m.build), SYNOPKGLAND_SPK_VERSION: version, SYNOPKGLAND_PROJECT_DIR: m.root };
|
|
278
|
+
if (m.native.prepare)
|
|
279
|
+
runHook(m, m.native.prepare, env, run);
|
|
280
|
+
const outputs = [];
|
|
281
|
+
for (const requested of arches) {
|
|
282
|
+
const arch = requested;
|
|
283
|
+
const toolkit = await toolkitFor(m, arch, run);
|
|
284
|
+
const stage = mkdtempSync(join(tmpdir(), 'synopkgland-layout-'));
|
|
285
|
+
try {
|
|
286
|
+
runHook(m, m.native.hook, { ...env, SYNOPKGLAND_ARCH: arch, SYNOPKGLAND_TOOLKIT_DIR: toolkit }, run);
|
|
287
|
+
const payload = inside(m.root, m.layout.payload.replaceAll('{arch}', arch));
|
|
288
|
+
const metadata = inside(m.root, m.layout.metadata);
|
|
289
|
+
checkLayoutMetadata(metadata, false, m.package);
|
|
290
|
+
layoutFiles(payload);
|
|
291
|
+
const binaries = m.layout.binaries.filter(b => !b.arches || b.arches.includes(arch));
|
|
292
|
+
if (!binaries.length)
|
|
293
|
+
fail("binary assertions required for every architecture");
|
|
294
|
+
for (const b of binaries)
|
|
295
|
+
checkLayoutBinary(payload, b, arch);
|
|
296
|
+
copyTree(metadata, stage);
|
|
297
|
+
const target = join(stage, 'payload');
|
|
298
|
+
mkdirSync(target);
|
|
299
|
+
const files = copyTree(payload, target);
|
|
300
|
+
await createReproducibleTar(target, join(stage, 'package.tgz'), { gzip: true });
|
|
301
|
+
rmSync(target, { recursive: true });
|
|
302
|
+
const fields = { package: m.package, version, arch, os_min_ver: m.os_min_ver, displayname: m.displayname, description: m.description, maintainer: m.maintainer, dsmuidir: 'ui', dsmappname: `SYNO.SDS.${m.package}.Instance`, startable: 'yes', thirdparty: 'yes', checksum: createHash('md5').update(readFileSync(join(stage, 'package.tgz'))).digest('hex') };
|
|
303
|
+
writeFileSync(join(stage, 'INFO'), Object.entries(fields).map(([k, v]) => `${k}="${v}"\n`).join(''));
|
|
304
|
+
writeFileSync(join(stage, 'native-layout.json'), JSON.stringify({ format: 1, arch, binaries: binaries.map(({ path, linkage }) => ({ path, linkage })), files }));
|
|
305
|
+
const output = join(options.outDir ?? join(m.root, 'dist'), `${m.package}-${version}-${arch}.spk`);
|
|
306
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
307
|
+
await createReproducibleTar(stage, output);
|
|
308
|
+
outputs.push(output);
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
rmSync(stage, { recursive: true, force: true });
|
|
312
|
+
if (toolkit)
|
|
313
|
+
rmSync(toolkit, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { outputs };
|
|
317
|
+
}
|
|
318
|
+
export function inspectNativeLayout(dir, payload, info) {
|
|
319
|
+
try {
|
|
320
|
+
if (!/^[A-Za-z][A-Za-z0-9]{1,63}$/.test(info.package ?? '') || !/^\d+(\.\d+){0,3}-\d+$/.test(info.version ?? '') || !/^7\.\d+(\.\d+)?-\d+$/.test(info.os_min_ver ?? ''))
|
|
321
|
+
fail('invalid native INFO identity/version/DSM');
|
|
322
|
+
const inv = object(JSON.parse(readFileSync(join(dir, 'native-layout.json'), 'utf8')), ['format', 'arch', 'binaries', 'files']);
|
|
323
|
+
if (inv.format !== 1 || inv.arch !== info.arch || !Object.hasOwn(LAYOUT_ARCHES, inv.arch))
|
|
324
|
+
fail('invalid native layout inventory');
|
|
325
|
+
checkLayoutMetadata(dir, true, info.package);
|
|
326
|
+
const actual = layoutFiles(payload);
|
|
327
|
+
if (JSON.stringify(actual) !== JSON.stringify(inv.files))
|
|
328
|
+
fail('payload checksum or mode mismatch');
|
|
329
|
+
if (!Array.isArray(inv.binaries) || !inv.binaries.length)
|
|
330
|
+
fail('missing binary assertions');
|
|
331
|
+
for (const b of inv.binaries) {
|
|
332
|
+
object(b, ['path', 'linkage']);
|
|
333
|
+
pathText(b.path);
|
|
334
|
+
if (!['static', 'dynamic'].includes(b.linkage))
|
|
335
|
+
fail('invalid linkage');
|
|
336
|
+
checkLayoutBinary(payload, b, inv.arch);
|
|
337
|
+
}
|
|
338
|
+
if (createHash('md5').update(readFileSync(join(dir, 'package.tgz'))).digest('hex') !== info.checksum)
|
|
339
|
+
fail('package checksum mismatch');
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
return [String(e)];
|
|
344
|
+
}
|
|
345
|
+
}
|