@synopackageland/cli 0.3.8 → 0.3.10

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared Folder permission pre-flight for the dynamic package-only route.
3
+ *
4
+ * Why this lives here and not in the SPK's preinst: preinst runs as the
5
+ * low-privilege package user, which cannot read ACL at all -- `synoacltool -get`
6
+ * answers `Access deny` and there is no CAP_SETUID to try the access as the
7
+ * selected user. The two places that *do* hold an administrator session are the
8
+ * install wizard and CLI `deploy`, so the check belongs to them. That makes it a
9
+ * pre-flight guard, not an enforcement gate: `deploy` can still be handed
10
+ * extra_values directly, and share permission can change after install.
11
+ *
12
+ * The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
13
+ * before changing the predicate: on vt-nas, `newadmin` has is_writable=false on
14
+ * its own row for `pkgland-p0b` and still writes there, because the
15
+ * `administrators` group row carries the grant. A user-row-only check rejects
16
+ * the only configuration with live proof behind it.
17
+ */
18
+ export interface SharePermissionRow {
19
+ name: string;
20
+ is_admin: boolean;
21
+ is_writable: boolean;
22
+ is_readonly: boolean;
23
+ is_deny: boolean;
24
+ is_custom: boolean;
25
+ }
26
+ export type ShareAccess = "write" | "read" | "none" | "denied" | "unknown";
27
+ export interface ShareAccessVerdict {
28
+ access: ShareAccess;
29
+ /** Which row granted access, as `user:<name>` or `group:<name>`. */
30
+ grantedBy?: string;
31
+ /** Which row denied access, as `user:<name>` or `group:<name>`. */
32
+ deniedBy?: string;
33
+ /** Why an `unknown` verdict could not be decided. */
34
+ reason?: "custom-acl" | "no-rows";
35
+ }
36
+ export interface ShareAccessInput {
37
+ userName: string;
38
+ /** Group names the user belongs to. Matched exactly -- no case folding. */
39
+ userGroups: readonly string[];
40
+ userRows: readonly SharePermissionRow[];
41
+ groupRows: readonly SharePermissionRow[];
42
+ }
43
+ export declare function evaluateShareAccess(input: ShareAccessInput): ShareAccessVerdict;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Shared Folder permission pre-flight for the dynamic package-only route.
3
+ *
4
+ * Why this lives here and not in the SPK's preinst: preinst runs as the
5
+ * low-privilege package user, which cannot read ACL at all -- `synoacltool -get`
6
+ * answers `Access deny` and there is no CAP_SETUID to try the access as the
7
+ * selected user. The two places that *do* hold an administrator session are the
8
+ * install wizard and CLI `deploy`, so the check belongs to them. That makes it a
9
+ * pre-flight guard, not an enforcement gate: `deploy` can still be handed
10
+ * extra_values directly, and share permission can change after install.
11
+ *
12
+ * The rows are SYNO.Core.Share.Permission v1 output verbatim. Read the tests
13
+ * before changing the predicate: on vt-nas, `newadmin` has is_writable=false on
14
+ * its own row for `pkgland-p0b` and still writes there, because the
15
+ * `administrators` group row carries the grant. A user-row-only check rejects
16
+ * the only configuration with live proof behind it.
17
+ */
18
+ function relevantRows(input) {
19
+ const groups = new Set(input.userGroups);
20
+ const candidates = [];
21
+ for (const row of input.userRows) {
22
+ if (row.name === input.userName) {
23
+ candidates.push({ row, label: `user:${row.name}` });
24
+ }
25
+ }
26
+ for (const row of input.groupRows) {
27
+ if (groups.has(row.name)) {
28
+ candidates.push({ row, label: `group:${row.name}` });
29
+ }
30
+ }
31
+ return candidates;
32
+ }
33
+ export function evaluateShareAccess(input) {
34
+ // An empty listing means the API answer was missing or truncated. Reading
35
+ // that as a denial would block installs on a transport error.
36
+ if (input.userRows.length === 0 && input.groupRows.length === 0) {
37
+ return { access: "unknown", reason: "no-rows" };
38
+ }
39
+ const candidates = relevantRows(input);
40
+ // Advanced/Windows ACL overrides share permission and only root-only
41
+ // synoacltool can read it, so any custom row in the deciding set makes the
42
+ // whole verdict inconclusive -- in both directions.
43
+ if (candidates.some(({ row }) => row.is_custom)) {
44
+ return { access: "unknown", reason: "custom-acl" };
45
+ }
46
+ const denied = candidates.find(({ row }) => row.is_deny);
47
+ if (denied) {
48
+ return { access: "denied", deniedBy: denied.label };
49
+ }
50
+ const writable = candidates.find(({ row }) => row.is_admin || row.is_writable);
51
+ if (writable) {
52
+ return { access: "write", grantedBy: writable.label };
53
+ }
54
+ const readable = candidates.find(({ row }) => row.is_readonly);
55
+ if (readable) {
56
+ return { access: "read", grantedBy: readable.label };
57
+ }
58
+ return { access: "none" };
59
+ }
@@ -1,4 +1,7 @@
1
+ import type { SharePermissionListing } from "../deploy/share-permission-preflight.js";
1
2
  import type { InstalledPackageState } from "./catalog-install.js";
3
+ import type { PortInfoEntry } from "./port-info.js";
4
+ import type { DsmPortProfileInventory } from "./dsm-port-profile.js";
2
5
  export type AttestationKind = "local" | "app-package" | "none";
3
6
  export type PackageRoute = "package-only" | "broker-installer" | "unknown";
4
7
  export type OidcOriginKind = "public-ca" | "quickconnect" | "lan-ip" | "self-signed" | "private-ca" | "http";
@@ -79,6 +82,16 @@ export interface NasHost {
79
82
  /** List the current logical Shared Folder names using an administrator session. */
80
83
  listShareNames?(): Promise<string[]>;
81
84
  resolveSharePath?(logicalName: string): Promise<string | undefined>;
85
+ /**
86
+ * Read SYNO.Core.Share.Permission for one Share using an administrator
87
+ * session. Returns undefined when the host cannot answer -- callers must
88
+ * treat that as "cannot tell", never as a denial.
89
+ */
90
+ listSharePermissions?(logicalName: string): Promise<SharePermissionListing | undefined>;
91
+ /** Group names a DSM user belongs to, for resolving share permission. */
92
+ listUserGroups?(userName: string): Promise<string[] | undefined>;
93
+ /** Read SYNO.Core.Service.PortInfo/load using an administrator session. */
94
+ listPortInfo?(): Promise<PortInfoEntry[] | undefined>;
82
95
  /** Resolve the NAS-specific user-home root when DSM User Home is enabled. */
83
96
  resolveHomesPath?(): Promise<string | undefined>;
84
97
  getInstalledProjectEnv?(packageIdentity: string): Promise<Record<string, string> | undefined>;
@@ -94,6 +107,12 @@ export interface NasHost {
94
107
  isPackageOnly?: boolean;
95
108
  [key: string]: unknown;
96
109
  } | undefined>;
110
+ /**
111
+ * Read DSM PortInfo plus the administrator-visible installed package list.
112
+ * This path does not require SynoPkgLandService; undefined means the host
113
+ * could not answer and must be treated as unknown by diagnostics.
114
+ */
115
+ getDsmPortProfileInventory?(): Promise<DsmPortProfileInventory | undefined>;
97
116
  /** Read the optional administrator OIDC compatibility settings endpoint. */
98
117
  getOidcCompatSettings?(): Promise<HostOidcCompatSettings | undefined>;
99
118
  smokeRequireLogin(input: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {