@synopackageland/cli 0.13.1 → 0.15.0

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +44 -1
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -850
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/app-launch.d.ts +11 -0
  14. package/dist/contract/app-launch.js +18 -0
  15. package/dist/contract/web-launch.d.ts +3 -13
  16. package/dist/contract/web-launch.js +14 -81
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/error-registry.js +1020 -831
  46. package/dist/info.js +120 -28
  47. package/dist/init.js +188 -64
  48. package/dist/inspect.js +21 -6
  49. package/dist/runtime/executor-digests.json +1 -12
  50. package/dist/skill.js +51 -9
  51. package/dist/standalone/api-description-inspect.d.ts +4 -0
  52. package/dist/standalone/api-description-inspect.js +12 -0
  53. package/dist/standalone/api-description.d.ts +7 -0
  54. package/dist/standalone/api-description.js +73 -0
  55. package/dist/standalone/build.d.ts +3 -0
  56. package/dist/standalone/build.js +140 -33
  57. package/dist/standalone/compose-errors.d.ts +3 -0
  58. package/dist/standalone/compose-errors.js +243 -0
  59. package/dist/standalone/compose.d.ts +97 -10
  60. package/dist/standalone/compose.js +306 -75
  61. package/dist/standalone/daemon.js +111 -29
  62. package/dist/standalone/dsm-window.js +55 -9
  63. package/dist/standalone/identity-gateway.d.ts +9 -2
  64. package/dist/standalone/identity-gateway.js +282 -66
  65. package/dist/standalone/inspect-spk.d.ts +15 -0
  66. package/dist/standalone/inspect-spk.js +714 -128
  67. package/dist/standalone/lifecycle.js +124 -42
  68. package/dist/standalone/manifest.d.ts +10 -1
  69. package/dist/standalone/manifest.js +308 -87
  70. package/dist/standalone/resource.js +6 -2
  71. package/dist/standalone/spk.d.ts +4 -1
  72. package/dist/standalone/spk.js +139 -39
  73. package/dist/template-paths.d.ts +0 -2
  74. package/dist/template-paths.js +0 -5
  75. package/dist/test-command.d.ts +1 -1
  76. package/dist/test-command.js +1 -1
  77. package/dist/validate.d.ts +3 -3
  78. package/dist/validate.js +32 -140
  79. package/package.json +5 -3
  80. package/schema/synology-app.schema.json +1 -1
  81. package/schema/synopkgland-api-v1.schema.json +423 -0
  82. package/skills/SKILL.md +34 -1
  83. package/skills/reference/compose.md +349 -0
  84. package/skills/reference/create.md +26 -4
  85. package/skills/reference/ctl.md +41 -0
  86. package/skills/reference/deploy.md +1 -1
  87. package/skills/reference/files.md +4 -0
  88. package/skills/reference/identity-gateway.md +8 -138
  89. package/skills/reference/portal.md +31 -0
  90. package/skills/reference/ui-vue.md +31 -0
  91. package/skills/reference/wrap.md +10 -30
  92. package/assets/go/daemon/fixture_linux_test.go +0 -15
  93. package/assets/go/daemon/fixture_other_test.go +0 -7
  94. package/assets/go/daemon/go.mod +0 -5
  95. package/assets/go/daemon/main.go +0 -636
  96. package/assets/go/daemon/main_test.go +0 -315
  97. package/assets/go/daemon/network.go +0 -107
  98. package/assets/go/daemon/network_test.go +0 -86
  99. package/assets/go/daemon/process_linux.go +0 -55
  100. package/assets/go/daemon/process_linux_test.go +0 -161
  101. package/assets/go/daemon/process_other.go +0 -14
  102. package/assets/go/daemon/testdata/child.go +0 -44
  103. package/assets/go/gateway/auth.go +0 -101
  104. package/assets/go/gateway/auth_test.go +0 -216
  105. package/assets/go/gateway/cgi.go +0 -133
  106. package/assets/go/gateway/cgi_test.go +0 -156
  107. package/assets/go/gateway/csrf.go +0 -73
  108. package/assets/go/gateway/csrf_test.go +0 -155
  109. package/assets/go/gateway/directory.go +0 -108
  110. package/assets/go/gateway/directory_test.go +0 -202
  111. package/assets/go/gateway/gateway.go +0 -412
  112. package/assets/go/gateway/gateway_test.go +0 -676
  113. package/assets/go/gateway/go.mod +0 -5
  114. package/assets/go/gateway/groups.go +0 -66
  115. package/assets/go/gateway/main.go +0 -45
  116. package/dist/build-spk/gateway.d.ts +0 -31
  117. package/dist/build-spk/gateway.js +0 -60
  118. package/dist/compose/privilege.d.ts +0 -14
  119. package/dist/compose/privilege.js +0 -102
@@ -1,66 +0,0 @@
1
- package main
2
-
3
- import (
4
- "bufio"
5
- "context"
6
- "fmt"
7
- "os"
8
- "slices"
9
- "strings"
10
- )
11
-
12
- // groupsOf reads local group files fresh on each request. Directory accounts
13
- // use NSS afresh; DSM provider caching may delay directory membership changes.
14
- // DSM keeps no member list for the group
15
- // that is an account's primary group (users), so membership is the member
16
- // list or the primary gid from the passwd file. Accounts absent from passwd use
17
- // DSM NSS; no local member-list fallback is permitted on lookup failure.
18
- func (a *dsmAuthenticator) groupsOf(ctx context.Context, user string) ([]string, error) {
19
- primary, err := a.primaryGID(user)
20
- if err != nil {
21
- return nil, err
22
- }
23
- if primary == "" {
24
- if a.directoryGroups == nil {
25
- return nil, fmt.Errorf("directory group lookup unavailable")
26
- }
27
- return a.directoryGroups(ctx, user)
28
- }
29
- f, err := os.Open(a.groupFile)
30
- if err != nil {
31
- return nil, fmt.Errorf("read groups: %w", err)
32
- }
33
- defer f.Close()
34
- var groups []string
35
- scanner := bufio.NewScanner(f)
36
- for scanner.Scan() {
37
- fields := strings.Split(scanner.Text(), ":")
38
- if len(fields) == 4 && (fields[2] == primary || slices.Contains(strings.Split(fields[3], ","), user)) {
39
- groups = append(groups, fields[0])
40
- }
41
- }
42
- if err := scanner.Err(); err != nil {
43
- return nil, fmt.Errorf("read groups: %w", err)
44
- }
45
- return groups, nil
46
- }
47
-
48
- // primaryGID is the account's gid in the passwd file; "" when the account has
49
- // no local entry, which requires a fresh directory lookup.
50
- func (a *dsmAuthenticator) primaryGID(user string) (string, error) {
51
- f, err := os.Open(a.passwdFile)
52
- if err != nil {
53
- return "", fmt.Errorf("read accounts: %w", err)
54
- }
55
- defer f.Close()
56
- scanner := bufio.NewScanner(f)
57
- for scanner.Scan() {
58
- if fields := strings.Split(scanner.Text(), ":"); len(fields) >= 4 && fields[0] == user {
59
- return fields[3], nil
60
- }
61
- }
62
- if err := scanner.Err(); err != nil {
63
- return "", fmt.Errorf("read accounts: %w", err)
64
- }
65
- return "", nil
66
- }
@@ -1,45 +0,0 @@
1
- // Command gateway is the DSM-authenticated front door of a container package.
2
- // DSM runs it as a CGI (ui/cgi/gateway.cgi in the package) under the package
3
- // account, once per request.
4
- package main
5
-
6
- import (
7
- "bytes"
8
- "encoding/json"
9
- "fmt"
10
- "log"
11
- "net/http/cgi"
12
- "os"
13
- )
14
-
15
- // Set by `synopkgland build-spk` through -ldflags -X; never read from a request.
16
- var (
17
- packageID = "dev"
18
- configJSON = ""
19
- )
20
-
21
- func main() {
22
- log.SetFlags(0)
23
- log.SetPrefix(packageID + ": ")
24
- if os.Getenv("GATEWAY_INTERFACE") == "" {
25
- fmt.Fprintf(os.Stderr, "%s: this program runs as the package's DSM CGI; open the app from the DSM main menu.\n", packageID)
26
- os.Exit(2)
27
- }
28
- var cfg config
29
- decoder := json.NewDecoder(bytes.NewReader([]byte(configJSON)))
30
- decoder.DisallowUnknownFields()
31
- if err := decoder.Decode(&cfg); err != nil {
32
- log.Fatalf("invalid gateway configuration: %v", err)
33
- }
34
- g, err := newGateway(cfg, newDSMAuthenticator())
35
- if err != nil {
36
- log.Fatal(err)
37
- }
38
- req, err := cgi.Request()
39
- if err != nil {
40
- log.Fatal(err)
41
- }
42
- if err := serveCGI(g, req, os.Stdin, os.Stdout); err != nil {
43
- log.Fatal(err)
44
- }
45
- }
@@ -1,31 +0,0 @@
1
- import { type CommandRunner } from "../standalone/build.js";
2
- import type { AppContract } from "../types.js";
3
- import type { NativeArchFamily } from "./dsm-arch.js";
4
- export { gatewayEntryPath } from "../contract/web-launch.js";
5
- /** Where the gateway CGI sits in the package payload; DSM serves `ui/` under /webman/3rdparty/<package>/. */
6
- export declare const GATEWAY_CGI_PAYLOAD = "ui/cgi/gateway.cgi";
7
- /** Readable copy of the baked-in configuration, for inspection; the CGI does not read it. */
8
- export declare const GATEWAY_CONFIG_PAYLOAD = "gateway.json";
9
- export type GatewayErrorCode = "GATEWAY_GO_MISSING" | "GATEWAY_BUILD_FAILED";
10
- export interface GatewayConfig {
11
- upstream: string;
12
- access: "users" | "admins" | "groups";
13
- groups?: string[];
14
- }
15
- /** The access rule and container address the gateway enforces; undefined for an App without one. */
16
- export declare function gatewayConfig(contract: AppContract, adminport: number): GatewayConfig | undefined;
17
- /** Compiles the fixed gateway module for `arch`; the result is a static ELF or a coded failure. */
18
- export declare function buildGatewayBinary(input: {
19
- packageId: string;
20
- arch: NativeArchFamily;
21
- config: GatewayConfig;
22
- work: string;
23
- run?: CommandRunner;
24
- }): Promise<{
25
- binary: string;
26
- } | {
27
- error: GatewayErrorCode;
28
- detail: string;
29
- }>;
30
- /** Puts the CGI (mode 0755) and its readable configuration into the package payload. */
31
- export declare function writeGatewayPayload(packageDir: string, binary: string, config: GatewayConfig): void;
@@ -1,60 +0,0 @@
1
- import { goChildEnv } from "../runtime/go-env.js";
2
- import { chmodSync, copyFileSync, mkdirSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { checkCgiBinary } from "../standalone/binary.js";
5
- import { runCommand } from "../standalone/build.js";
6
- import { resolvedWebAccess } from "../contract/web-launch.js";
7
- import { goAssetDir } from "../template-paths.js";
8
- import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from "./go-toolchain.js";
9
- export { gatewayEntryPath } from "../contract/web-launch.js";
10
- /** Where the gateway CGI sits in the package payload; DSM serves `ui/` under /webman/3rdparty/<package>/. */
11
- export const GATEWAY_CGI_PAYLOAD = "ui/cgi/gateway.cgi";
12
- /** Readable copy of the baked-in configuration, for inspection; the CGI does not read it. */
13
- export const GATEWAY_CONFIG_PAYLOAD = "gateway.json";
14
- const ACCESS = { "dsm-users": "users", "dsm-admins": "admins", "dsm-groups": "groups" };
15
- /** The access rule and container address the gateway enforces; undefined for an App without one. */
16
- export function gatewayConfig(contract, adminport) {
17
- const access = resolvedWebAccess(contract);
18
- if (!access || access.mode === "public") {
19
- return undefined;
20
- }
21
- return {
22
- upstream: `127.0.0.1:${adminport}`,
23
- access: ACCESS[access.mode],
24
- ...(access.mode === "dsm-groups" ? { groups: access.groups } : {}),
25
- };
26
- }
27
- /** Compiles the fixed gateway module for `arch`; the result is a static ELF or a coded failure. */
28
- export async function buildGatewayBinary(input) {
29
- const out = join(input.work, `gateway-${input.arch}.cgi`);
30
- const ldflags = ["-s", "-w", "-buildid=", `-X main.packageID=${input.packageId}`, `-X 'main.configJSON=${JSON.stringify(input.config)}'`].join(" ");
31
- const result = await (input.run ?? runCommand)("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, "."], {
32
- cwd: goAssetDir("gateway"),
33
- env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(input.arch) }),
34
- });
35
- if (result.missing) {
36
- return { error: "GATEWAY_GO_MISSING", detail: `The go command was not found. Building the DSM verification gateway needs Go 1.21 or later (https://go.dev/dl/). ${PINNED_GO_HINT}` };
37
- }
38
- if (result.status !== 0) {
39
- return { error: "GATEWAY_BUILD_FAILED", detail: `go build (${input.arch}) failed. ${PINNED_GO_HINT}\n${result.stderr}` };
40
- }
41
- const problem = checkCgiBinary(input.work, out, input.arch);
42
- if (problem) {
43
- return { error: "GATEWAY_BUILD_FAILED", detail: problem.message };
44
- }
45
- const toolchainProblem = pinnedGoProblem(out);
46
- if (toolchainProblem) {
47
- return { error: "GATEWAY_BUILD_FAILED", detail: toolchainProblem };
48
- }
49
- return { binary: out };
50
- }
51
- /** Puts the CGI (mode 0755) and its readable configuration into the package payload. */
52
- export function writeGatewayPayload(packageDir, binary, config) {
53
- const cgi = join(packageDir, GATEWAY_CGI_PAYLOAD);
54
- mkdirSync(join(cgi, ".."), { recursive: true });
55
- copyFileSync(binary, cgi);
56
- chmodSync(cgi, 0o755);
57
- const configPath = join(packageDir, GATEWAY_CONFIG_PAYLOAD);
58
- writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
59
- chmodSync(configPath, 0o644);
60
- }
@@ -1,14 +0,0 @@
1
- import type { AppContract, ComposeDocument } from "../types.js";
2
- export declare const PRIVILEGE_FLAGS: readonly ["privileged", "host_network", "host_pid", "host_ipc", "devices", "cap_add"];
3
- export type PrivilegeFlag = (typeof PRIVILEGE_FLAGS)[number];
4
- export interface PrivilegeFinding {
5
- flag: PrivilegeFlag;
6
- code: string;
7
- serviceName: string;
8
- path: string;
9
- message: string;
10
- suggestion: string;
11
- }
12
- export declare function declaredPrivilegeFlags(contract: AppContract): Set<PrivilegeFlag>;
13
- export declare function scanComposePrivilege(compose: ComposeDocument): PrivilegeFinding[];
14
- export declare function undeclaredPrivilegeFindings(compose: ComposeDocument, declared: ReadonlySet<PrivilegeFlag>): PrivilegeFinding[];
@@ -1,102 +0,0 @@
1
- export const PRIVILEGE_FLAGS = [
2
- "privileged",
3
- "host_network",
4
- "host_pid",
5
- "host_ipc",
6
- "devices",
7
- "cap_add",
8
- ];
9
- const FLAG_ERRORS = {
10
- privileged: {
11
- code: "COMPOSE_PRIVILEGED_FORBIDDEN",
12
- message: "Compose uses a privileged container but the contract does not declare privilege.privileged.",
13
- suggestion: "Add privilege.privileged: true to synology-app.yaml, or remove privileged: true.",
14
- },
15
- host_network: {
16
- code: "COMPOSE_HOST_NETWORK_FORBIDDEN",
17
- message: "Compose uses host network but the contract does not declare privilege.host_network.",
18
- suggestion: "Add privilege.host_network: true to synology-app.yaml, or use a controlled network mode.",
19
- },
20
- host_pid: {
21
- code: "COMPOSE_HOST_PID_FORBIDDEN",
22
- message: "Compose uses host PID but the contract does not declare privilege.host_pid.",
23
- suggestion: "Add privilege.host_pid: true to synology-app.yaml, or remove pid: host.",
24
- },
25
- host_ipc: {
26
- code: "COMPOSE_HOST_IPC_FORBIDDEN",
27
- message: "Compose uses host IPC but the contract does not declare privilege.host_ipc.",
28
- suggestion: "Add privilege.host_ipc: true to synology-app.yaml, or remove ipc: host.",
29
- },
30
- devices: {
31
- code: "COMPOSE_DEVICES_FORBIDDEN",
32
- message: "Compose mounts devices but the contract does not declare privilege.devices.",
33
- suggestion: "Add privilege.devices: true to synology-app.yaml, or remove devices.",
34
- },
35
- cap_add: {
36
- code: "COMPOSE_CAP_ADD_FORBIDDEN",
37
- message: "Compose uses extra cap_add but the contract does not declare privilege.cap_add.",
38
- suggestion: "Add privilege.cap_add: true to synology-app.yaml, or remove cap_add.",
39
- },
40
- };
41
- export function declaredPrivilegeFlags(contract) {
42
- const raw = contract.privilege;
43
- const flags = new Set();
44
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
45
- return flags;
46
- }
47
- const record = raw;
48
- for (const flag of PRIVILEGE_FLAGS) {
49
- if (record[flag] === true) {
50
- flags.add(flag);
51
- }
52
- }
53
- return flags;
54
- }
55
- export function scanComposePrivilege(compose) {
56
- const findings = [];
57
- if (!compose.services) {
58
- return findings;
59
- }
60
- for (const [serviceName, service] of Object.entries(compose.services)) {
61
- const basePath = `services.${serviceName}`;
62
- const add = (flag, path) => {
63
- const spec = FLAG_ERRORS[flag];
64
- findings.push({
65
- flag,
66
- code: spec.code,
67
- serviceName,
68
- path,
69
- message: spec.message,
70
- suggestion: spec.suggestion,
71
- });
72
- };
73
- if (service.privileged === true) {
74
- add("privileged", `${basePath}.privileged`);
75
- }
76
- if (service.network_mode === "host") {
77
- add("host_network", `${basePath}.network_mode`);
78
- }
79
- if (service.pid === "host") {
80
- add("host_pid", `${basePath}.pid`);
81
- }
82
- if (service.ipc === "host") {
83
- add("host_ipc", `${basePath}.ipc`);
84
- }
85
- if (hasEntries(service.devices)) {
86
- add("devices", `${basePath}.devices`);
87
- }
88
- if (hasEntries(service.cap_add)) {
89
- add("cap_add", `${basePath}.cap_add`);
90
- }
91
- }
92
- return findings;
93
- }
94
- export function undeclaredPrivilegeFindings(compose, declared) {
95
- return scanComposePrivilege(compose).filter((item) => !declared.has(item.flag));
96
- }
97
- function hasEntries(value) {
98
- if (Array.isArray(value)) {
99
- return value.length > 0;
100
- }
101
- return typeof value === "string" && value.trim() !== "";
102
- }