@synopackageland/cli 0.41.12 → 0.42.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.
- package/CHANGELOG.md +43 -0
- package/README.md +27 -12
- package/dist/ctl/cli.d.ts +5 -0
- package/dist/ctl/cli.js +12 -5
- package/dist/ctl/descriptor.js +2 -1
- package/dist/ctl/input.d.ts +5 -1
- package/dist/ctl/input.js +28 -14
- package/dist/ctl/pattern.d.ts +12 -0
- package/dist/ctl/pattern.js +10 -0
- package/dist/ctl/redaction.js +21 -2
- package/dist/ctl/service.d.ts +17 -5
- package/dist/ctl/service.js +33 -22
- package/dist/e2e/app-open.js +5 -1
- package/dist/error-registry.js +2 -2
- package/dist/host/package-commands.d.ts +15 -0
- package/dist/host/package-commands.js +15 -4
- package/dist/runtime/sdk-source.d.ts +2 -0
- package/dist/runtime/sdk-source.js +2 -0
- package/dist/spk/archive-inventory.d.ts +9 -0
- package/dist/spk/archive-inventory.js +65 -0
- package/dist/spk/check-payload.js +9 -0
- package/dist/spk/rules.d.ts +5 -0
- package/dist/spk/rules.js +1 -0
- package/dist/spk/validate-spk.js +12 -0
- package/dist/standalone/compose.js +4 -4
- package/dist/standalone/daemon-ingress.d.ts +3 -2
- package/dist/standalone/daemon-ingress.js +12 -9
- package/dist/standalone/daemon-restart.d.ts +1 -1
- package/dist/standalone/daemon-restart.js +3 -3
- package/dist/standalone/daemon.d.ts +2 -2
- package/dist/standalone/daemon.js +5 -5
- package/dist/standalone/inspect-bridge.d.ts +3 -1
- package/dist/standalone/inspect-bridge.js +6 -3
- package/dist/standalone/inspect-spk.js +27 -7
- package/dist/standalone/install-settings.js +2 -2
- package/dist/standalone/manifest.js +1 -1
- package/dist/standalone/native-layout.js +18 -1
- package/dist/template-paths.d.ts +0 -4
- package/dist/template-paths.js +0 -11
- package/docs/ctl-external.md +16 -0
- package/package.json +3 -2
- package/skills/SKILL.md +1 -1
- package/skills/reference/ctl.md +24 -0
- package/skills/reference/daemon-mode.md +5 -0
- package/skills/reference/install.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.42.0 (2026-11-01)
|
|
6
|
+
|
|
7
|
+
- Require templates ~0.37.0 so `init` and `add` scaffold with UI 0.19.1 and
|
|
8
|
+
SDK/App 0.41.1 (executor/v0.39.0). CLI 0.41.x stays on the templates 0.35
|
|
9
|
+
minor line.
|
|
10
|
+
- Pin CLI-owned runtime defaults (daemon controllers, Compose planning and
|
|
11
|
+
installer context) to SDK 0.41.1, matching the templates. Explicit App SDK
|
|
12
|
+
declarations still take precedence.
|
|
13
|
+
- Skip installation when the requested package version is already installed,
|
|
14
|
+
including DSM build suffixes; report `action: "none"` in plans and avoid
|
|
15
|
+
downloads, uploads and reinstalling on confirmed runs.
|
|
16
|
+
- Accept repeated ctl flags for arrays of scalar values, validate their item
|
|
17
|
+
schemas and expose repeatable flags in command help and examples. Preserve
|
|
18
|
+
sensitive-value redaction and JSON input for complex parameters.
|
|
19
|
+
- Return `CTL_APP_NEEDS_APPROVAL` (exit 4) with an App-scoped
|
|
20
|
+
`ctl list --app <package> --approve-installed` hint when an installed App,
|
|
21
|
+
called by package ID or a cached alias, awaits first, new-version or
|
|
22
|
+
changed-description approval. Cached aliases identify upgraded packages but
|
|
23
|
+
never authorize execution; `CTL_APP_UNKNOWN` suggestions apply only to names
|
|
24
|
+
matching no installed package or known alias.
|
|
25
|
+
|
|
26
|
+
## 0.41.13
|
|
27
|
+
|
|
28
|
+
- Pin CLI-owned runtime defaults to SDK 0.41.0 independently of templates
|
|
29
|
+
- Check daemon restart support through the policy decoder and normalizer
|
|
30
|
+
- Bind daemon ingress to the exact, case-sensitive `/synopkgland/<package>/`
|
|
31
|
+
namespace across manifest validation, generated proxies, daemon environment,
|
|
32
|
+
SPK inspection and App smoke entry selection.
|
|
33
|
+
- Use bounded RE2 patterns for ctl schema validation and sensitive-value
|
|
34
|
+
redaction; reject unsupported validation patterns and redact conservatively
|
|
35
|
+
when an unvalidated privacy pattern cannot be compiled.
|
|
36
|
+
- Restrict native layout systemd units to package-process directives; reject
|
|
37
|
+
manager actions, activation dependencies and unknown directives or sections.
|
|
38
|
+
- Inspect bridge access against the declared package entry and validate its
|
|
39
|
+
permissions policy even when its embedded digest matches.
|
|
40
|
+
- Require executable native lifecycle scripts and the exact package privilege
|
|
41
|
+
declaration.
|
|
42
|
+
- Inventory both archives before SDK package extraction; flag duplicate names,
|
|
43
|
+
case or Unicode collisions, noncanonical paths and unexpected entry types,
|
|
44
|
+
including type flags otherwise ignored by the tar reader.
|
|
45
|
+
|
|
3
46
|
## 0.41.12 (2026-10-31)
|
|
4
47
|
|
|
5
48
|
- Remove the delegated WebAPI profile API-name and method-name word lists so
|
package/README.md
CHANGED
|
@@ -47,7 +47,8 @@ See the [upstream scaffold and release checklist](skills/reference/upstream-fork
|
|
|
47
47
|
| `compose` | Unchanged upstream Compose, generated override and SDK planner | `dsm-package.yaml` |
|
|
48
48
|
|
|
49
49
|
Existing native products with their own payload layout use `backend: {kind: native}` in
|
|
50
|
-
`dsm-package.yaml`.
|
|
50
|
+
`dsm-package.yaml`. Their systemd units accept only the package-process settings
|
|
51
|
+
listed in [native systemd settings](docs/native-layout-systemd.md).
|
|
51
52
|
|
|
52
53
|
Declare language-independent DSM installation requirements with
|
|
53
54
|
[`dependencies.packages`](docs/manifest.md). They merge with inferred Node
|
|
@@ -88,8 +89,9 @@ alongside npm lockfiles. Do not disable checksum verification for public SDK
|
|
|
88
89
|
packages. Private mirrors can use explicitly configured Go authentication and
|
|
89
90
|
checksum settings; a failed fetch reports `SDK_DOWNLOAD_FAILED`.
|
|
90
91
|
|
|
91
|
-
CLI
|
|
92
|
-
|
|
92
|
+
The CLI pins SDK 0.41.1 for its daemon controllers, Compose planning and installer
|
|
93
|
+
context. Explicit App SDK declarations take precedence. Templates use ~0.37.0
|
|
94
|
+
and retain their own scaffold SDK pins; none declare daemon ingress.
|
|
93
95
|
Every App method must be declared in the permissions table. Dynamic names and
|
|
94
96
|
custom HTTP dispatchers are rejected. With SDK 0.33.0, use fixed
|
|
95
97
|
`api.HandleHTTP` prefixes for authorized raw HTTP and `ipc.ServeUnix` for private
|
|
@@ -99,8 +101,8 @@ unimported development commands are excluded. Use
|
|
|
99
101
|
authentication. See [App entry and registration](docs/app-entry.md).
|
|
100
102
|
|
|
101
103
|
`init` and `add` read their templates from the locally installed
|
|
102
|
-
`@synopackageland/templates`. CLI 0.
|
|
103
|
-
update can select the latest templates patch in the 0.
|
|
104
|
+
`@synopackageland/templates`. CLI 0.42.0 depends on `~0.37.0`: npm installation or
|
|
105
|
+
update can select the latest templates patch in the 0.37 minor line. Templates
|
|
104
106
|
patches need only a templates release; update the npm installation to receive them.
|
|
105
107
|
The CLI never fetches templates at runtime, so offline use works after installation;
|
|
106
108
|
commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
|
|
@@ -109,8 +111,8 @@ exact SDK Go module sum anchors the build manifest; npm integrity anchors the
|
|
|
109
111
|
identical manifest in `@synopackageland/sdk/executor-manifest.json`. No manifest
|
|
110
112
|
is fetched at NAS runtime. Missing, malformed or unsupported new manifests fail
|
|
111
113
|
closed. SDK 0.26.0 and 0.27.0 retain the CLI's frozen legacy inventory.
|
|
112
|
-
|
|
113
|
-
release
|
|
114
|
+
CLI runtime defaults use its pinned SDK independently of the templates. An SDK minor
|
|
115
|
+
release does not require a templates release; new CLI capabilities or a new manifest
|
|
114
116
|
format can still require one.
|
|
115
117
|
|
|
116
118
|
For development against local checkouts, set `SYNOPKGLAND_TEMPLATES_DIR` to a
|
|
@@ -535,7 +537,7 @@ for `ctl` calls; App command/job/transfer deadlines retain their existing defaul
|
|
|
535
537
|
| 1 | Unexpected internal error (`CTL_INTERNAL`), or an uncaught CLI exception. |
|
|
536
538
|
| 2 | Authentication/session failure: `CTL_NOT_AUTHENTICATED`, `CTL_AUTH_FAILED`, `CTL_CREDENTIAL_MISMATCH`, or unavailable OS credential storage. |
|
|
537
539
|
| 3 | Invalid parameters, schema validation or unknown names: `CTL_PARAMS_INVALID`, `CTL_ARGUMENT_INVALID`, `CTL_APP_UNKNOWN`, `CTL_COMMAND_UNKNOWN`. |
|
|
538
|
-
| 4 |
|
|
540
|
+
| 4 | Approval/trust or permission refusal: `CTL_APP_NEEDS_APPROVAL`, `CTL_PERMISSION_DENIED`, `CTL_INVENTORY_DENIED`. |
|
|
539
541
|
| 5 | App/contract failure that fails the command: `CTL_APP_ERROR`, `CTL_RESPONSE_INVALID`, `CTL_JOB_FAILED`. |
|
|
540
542
|
| 6 | Network/HTTP failure or uncertain completion: `CTL_NETWORK`, `CTL_HTTP_ERROR`, `CTL_JOB_TIMEOUT`, `CTL_LOGOUT_FAILED`. |
|
|
541
543
|
| 7 | Confirmation required (`CTL_CONFIRM_REQUIRED`): a write/destructive command or host removal without confirmation; no business operation runs. |
|
|
@@ -545,13 +547,26 @@ For example, `synopkgland ctl studio drafts create --host nas --json` without
|
|
|
545
547
|
`--yes` returns `ok:false` with `CTL_CONFIRM_REQUIRED` and exits 7 once discovery
|
|
546
548
|
and parameter validation succeed. Missing sessions fail earlier with exit 2.
|
|
547
549
|
Discovery catches per-App description errors and excludes those Apps from its
|
|
548
|
-
results. For example, a changed pinned description (`
|
|
550
|
+
results. For example, a changed pinned description (`CTL_APP_NEEDS_APPROVAL`)
|
|
549
551
|
or an invalid descriptor (`CTL_DESCRIPTOR_INVALID`) appears in
|
|
550
552
|
`ctl list --host nas --refresh --json` under `data.diagnostics`; the listing still
|
|
551
553
|
returns `ok:true` and exits 0, even if no Apps remain. These diagnostic codes do
|
|
552
|
-
not become process exit codes 4 or 5
|
|
553
|
-
|
|
554
|
-
|
|
554
|
+
not become process exit codes 4 or 5 for a listing. Executing or requesting help
|
|
555
|
+
for an installed App by package ID or a known cached alias instead returns its
|
|
556
|
+
specific error; no business operation runs. A first approval, new version, or
|
|
557
|
+
changed description returns `CTL_APP_NEEDS_APPROVAL` and exit 4, for example
|
|
558
|
+
`SynoReclaim 1.2.0 needs approval`, with the exact App-scoped hint:
|
|
559
|
+
|
|
560
|
+
```sh
|
|
561
|
+
synopkgland ctl list --app SynoReclaim --approve-installed --host <profile>
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Obtain the user's authorization before approving. Previously approved aliases
|
|
565
|
+
identify upgraded installed packages without granting permission to execute.
|
|
566
|
+
Use the package ID for first approval when no alias has been discovered yet.
|
|
567
|
+
Only a name matching no installed package ID or known alias returns
|
|
568
|
+
`CTL_APP_UNKNOWN` with “Did you mean …” suggestions. Other description failures,
|
|
569
|
+
such as `CTL_DESCRIPTOR_INVALID`, remain specific errors (exit 5).
|
|
555
570
|
|
|
556
571
|
`host list/show` can also succeed while reporting `authenticated:false` and
|
|
557
572
|
exit 0. Check discovery `apps`/`diagnostics` and session fields as well as the exit
|
package/dist/ctl/cli.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface CtlIO {
|
|
|
12
12
|
export declare function listView(host: string, apps: Descriptor[]): {
|
|
13
13
|
commands: {
|
|
14
14
|
parameters: string | Record<string, any>;
|
|
15
|
+
flags: {
|
|
16
|
+
flag: string;
|
|
17
|
+
valueType: string;
|
|
18
|
+
repeatable: boolean;
|
|
19
|
+
}[];
|
|
15
20
|
examples: string[];
|
|
16
21
|
summary: string;
|
|
17
22
|
cli: string[];
|
package/dist/ctl/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Writable } from 'node:stream';
|
|
|
6
6
|
import { CtlError, errorData, ctlError } from './errors.js';
|
|
7
7
|
import { CtlStore } from './store.js';
|
|
8
8
|
import { CtlClient, boundedMap } from './service.js';
|
|
9
|
-
import { selectHosts,
|
|
9
|
+
import { selectHosts, selectOperation, flagName, flagValueType, RESERVED_FLAGS } from './input.js';
|
|
10
10
|
import { canonicalOrigin, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
11
11
|
import { loginDsm, logoutDsm } from './dsm-auth.js';
|
|
12
12
|
import { publicOperation } from './redaction.js';
|
|
@@ -51,16 +51,23 @@ function take(args, flag, value = false) {
|
|
|
51
51
|
return answer;
|
|
52
52
|
}
|
|
53
53
|
function q(value) { return "'" + value.replaceAll("'", "'\\''") + "'"; }
|
|
54
|
+
function typedFlags(op) {
|
|
55
|
+
const fields = Object.entries(op.paramsSchema?.properties ?? {}), names = fields.map(([key]) => flagName(key));
|
|
56
|
+
if (names.some(name => RESERVED_FLAGS.has(name)) || new Set(names).size !== names.length)
|
|
57
|
+
return [];
|
|
58
|
+
return fields.flatMap(([key, schema]) => /^[A-Za-z][A-Za-z0-9_]*$/.test(key) && flagValueType(schema) ? [{ flag: '--' + flagName(key), valueType: flagValueType(schema), repeatable: schema.type === 'array' }] : []);
|
|
59
|
+
}
|
|
54
60
|
function example(host, app, op) {
|
|
55
61
|
const base = `synopkgland ctl ${app.payload.aliases[0] ?? app.payload.package.id} ${op.cli.join(' ')} --host ${host}`;
|
|
62
|
+
const flags = typedFlags(op);
|
|
56
63
|
return (op.examples?.length ? op.examples : [{}]).map(params => {
|
|
57
|
-
const simple = op.paramsSchema && Object.entries(params).every(([key, value]) => ['string', 'number', 'boolean'].includes(typeof
|
|
64
|
+
const simple = op.paramsSchema && Object.entries(params).every(([key, value]) => { const flag = flags.find(f => f.flag === '--' + flagName(key)); return flag && (flag.repeatable ? Array.isArray(value) && value.length > 0 && value.every(v => ['string', 'number', 'boolean'].includes(typeof v)) : ['string', 'number', 'boolean'].includes(typeof value)); });
|
|
58
65
|
if (simple)
|
|
59
|
-
return base + Object.entries(params).
|
|
66
|
+
return base + Object.entries(params).flatMap(([key, value]) => (Array.isArray(value) ? value : [value]).map(item => ' --' + flagName(key) + ' ' + q(String(item)))).join('') + (op.effect === 'read' ? '' : ' --yes');
|
|
60
67
|
return `printf '%s' ${q(JSON.stringify(params))} | ${base} --input -` + (op.effect === 'read' ? '' : ' --yes');
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
|
-
export function listView(host, apps) { return apps.map(app => ({ host, package: app.payload.package, aliases: app.payload.aliases, protocolVersion: app.version, developedBy: app.payload.provenance.developedBy, provenance: 'self-declared', hash: app.sha256, ...(app.payload.external ? { external: app.payload.external } : {}), commands: Object.entries(app.payload.commands).map(([id, original]) => { const op = publicOperation(original); return { id, ...op, parameters: op.paramsSchema ?? 'Undescribed parameters; use --input', examples: example(host, app, op) }; }) })); }
|
|
70
|
+
export function listView(host, apps) { return apps.map(app => ({ host, package: app.payload.package, aliases: app.payload.aliases, protocolVersion: app.version, developedBy: app.payload.provenance.developedBy, provenance: 'self-declared', hash: app.sha256, ...(app.payload.external ? { external: app.payload.external } : {}), commands: Object.entries(app.payload.commands).map(([id, original]) => { const op = publicOperation(original); return { id, ...op, parameters: op.paramsSchema ?? 'Undescribed parameters; use --input', flags: typedFlags(op), examples: example(host, app, op) }; }) })); }
|
|
64
71
|
export async function runCtl(argv, io = {}) {
|
|
65
72
|
if (argv[0] === 'external')
|
|
66
73
|
return runExternal(argv.slice(1), io);
|
|
@@ -237,7 +244,7 @@ async function runCtlWithin(argv, io = {}) {
|
|
|
237
244
|
if (args[0] === 'help' || help) {
|
|
238
245
|
const nameArg = args[0] === 'help' ? args[1] : args[0];
|
|
239
246
|
const data = await ctl.discover(discoverOptions);
|
|
240
|
-
const app = selectApp(data
|
|
247
|
+
const app = ctl.selectApp(data, nameArg ?? '');
|
|
241
248
|
const path = args[0] === 'help' ? args.slice(2) : args.slice(1);
|
|
242
249
|
const view = listView(name, [app])[0];
|
|
243
250
|
return { host: name, ok: true, data: path.length ? { ...view, commands: [view.commands.find(c => c.id === selectOperation(app, path).id)] } : view };
|
package/dist/ctl/descriptor.js
CHANGED
|
@@ -3,9 +3,10 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { canonicalize } from 'json-canonicalize';
|
|
4
4
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
5
5
|
import { CtlError } from './errors.js';
|
|
6
|
+
import { schemaRegExp } from './pattern.js';
|
|
6
7
|
export const DESCRIPTION_METHOD = 'synopkgland.describe';
|
|
7
8
|
export const DESCRIPTION_FILE = 'synopkgland-api.json';
|
|
8
|
-
export const ajv = new Ajv2020.default({ allErrors: true, strict: false, validateFormats: false, addUsedSchema: false });
|
|
9
|
+
export const ajv = new Ajv2020.default({ allErrors: true, strict: false, validateFormats: false, addUsedSchema: false, code: { regExp: schemaRegExp } });
|
|
9
10
|
const descriptorSchema = ajv.compile(JSON.parse(readFileSync(new URL('../../schema/synopkgland-api-v1.schema.json', import.meta.url), 'utf8')));
|
|
10
11
|
export function makeDescriptor(payload) { const value = { format: 'synopkgland.api', version: 1, payload }; return { ...value, sha256: createHash('sha256').update(canonicalize(value)).digest('hex') }; }
|
|
11
12
|
export function validateDescriptor(value, id, version) {
|
package/dist/ctl/input.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type Descriptor, type Operation } from './descriptor.js';
|
|
2
2
|
export declare function selectHosts(names: string[], selected?: string, all?: boolean): string[];
|
|
3
|
-
export declare function selectApp
|
|
3
|
+
export declare function selectApp<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
aliases: string[];
|
|
6
|
+
}>(apps: T[], name: string): T;
|
|
4
7
|
export declare function selectOperation(app: Descriptor, path: string[]): {
|
|
5
8
|
id: string;
|
|
6
9
|
operation: Operation;
|
|
7
10
|
};
|
|
8
11
|
export declare const RESERVED_FLAGS: Set<string>;
|
|
9
12
|
export declare function flagName(key: string): string;
|
|
13
|
+
export declare function flagValueType(schema: any): string | undefined;
|
|
10
14
|
export declare function operationInput(op: Operation, args: string[], stdin?: string, helpHint?: string): Record<string, unknown>;
|
|
11
15
|
export declare function requestPlan(app: Descriptor, id: string, params: Record<string, unknown>): {
|
|
12
16
|
package: string;
|
package/dist/ctl/input.js
CHANGED
|
@@ -21,12 +21,15 @@ export function selectHosts(names, selected, all = false) {
|
|
|
21
21
|
return [...new Set(hosts)];
|
|
22
22
|
}
|
|
23
23
|
export function selectApp(apps, name) {
|
|
24
|
-
const
|
|
24
|
+
const exact = apps.find(app => app.id.toLowerCase() === name.toLowerCase());
|
|
25
|
+
if (exact)
|
|
26
|
+
return exact;
|
|
27
|
+
const found = apps.filter(app => app.aliases.some(alias => alias.toLowerCase() === name.toLowerCase()));
|
|
25
28
|
if (found.length > 1)
|
|
26
|
-
throw new CtlError('CTL_APP_AMBIGUOUS', 'Alias matches multiple packages.', 'Use an exact DSM package ID.', 3, found.map(
|
|
29
|
+
throw new CtlError('CTL_APP_AMBIGUOUS', 'Alias matches multiple packages.', 'Use an exact DSM package ID.', 3, found.map(app => app.id));
|
|
27
30
|
if (found.length === 1)
|
|
28
31
|
return found[0];
|
|
29
|
-
return choose(name, apps.flatMap(
|
|
32
|
+
return choose(name, apps.flatMap(app => [app.id, ...app.aliases]), 'app');
|
|
30
33
|
}
|
|
31
34
|
export function selectOperation(app, path) {
|
|
32
35
|
const found = Object.entries(app.payload.commands).find(([, op]) => op.cli.join(' ') === path.join(' '));
|
|
@@ -36,6 +39,10 @@ export function selectOperation(app, path) {
|
|
|
36
39
|
}
|
|
37
40
|
export const RESERVED_FLAGS = new Set(['ca', 'strict-tls', 'host', 'all-hosts', 'json', 'app', 'refresh', 'yes', 'dry-run', 'approve-installed', 'help', 'input', 'wait', 'no-wait', 'timeout']);
|
|
38
41
|
export function flagName(key) { return key.replace(/[A-Z]/g, c => '-' + c.toLowerCase()).replaceAll('_', '-'); }
|
|
42
|
+
export function flagValueType(schema) {
|
|
43
|
+
const type = schema?.type === 'array' ? schema.items?.type : schema?.type;
|
|
44
|
+
return ['string', 'integer', 'number', 'boolean'].includes(type) ? type : undefined;
|
|
45
|
+
}
|
|
39
46
|
export function operationInput(op, args, stdin, helpHint) {
|
|
40
47
|
const fail = (message, allowed) => { throw new CtlError('CTL_PARAMS_INVALID', message, helpHint ?? ('run: synopkgland ctl <package> ' + op.cli.join(' ') + ' --help'), 3, allowed); };
|
|
41
48
|
const inputIndex = args.indexOf('--input');
|
|
@@ -78,35 +85,42 @@ export function operationInput(op, args, stdin, helpHint) {
|
|
|
78
85
|
const field = fields.get(flag);
|
|
79
86
|
if (!field)
|
|
80
87
|
return fail('Unknown parameter flag.', [...fields.keys()]);
|
|
81
|
-
const [key, schema] = field;
|
|
82
|
-
if (Object.hasOwn(params, key))
|
|
88
|
+
const [key, schema] = field, repeatable = schema.type === 'array', type = flagValueType(schema);
|
|
89
|
+
if (Object.hasOwn(params, key) && !repeatable)
|
|
83
90
|
return fail(`Duplicate parameter flag ${flag}.`);
|
|
84
|
-
if (!
|
|
91
|
+
if (!type)
|
|
85
92
|
return fail(`Complex parameter ${flag}: use --input.`);
|
|
86
93
|
let value = split > 0 ? args[i].slice(split + 1) : undefined;
|
|
87
|
-
if (value === undefined &&
|
|
94
|
+
if (value === undefined && type === 'boolean' && (!args[i + 1] || args[i + 1].startsWith('--')))
|
|
88
95
|
value = 'true';
|
|
96
|
+
else if (value === undefined && (!args[i + 1] || args[i + 1].startsWith('--')))
|
|
97
|
+
return fail(`Missing parameter value for ${flag}.`);
|
|
89
98
|
else if (value === undefined)
|
|
90
99
|
value = args[++i];
|
|
91
100
|
if (value === undefined)
|
|
92
101
|
return fail(`Missing parameter value for ${flag}.`);
|
|
93
|
-
|
|
102
|
+
let parsed = value;
|
|
103
|
+
if (type === 'integer') {
|
|
94
104
|
if (!/^-?\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
95
105
|
return fail(`Parameter ${flag} must be a safe integer.`);
|
|
96
|
-
|
|
106
|
+
parsed = Number(value);
|
|
97
107
|
}
|
|
98
|
-
else if (
|
|
108
|
+
else if (type === 'number') {
|
|
99
109
|
if (!value.trim() || !Number.isFinite(Number(value)))
|
|
100
110
|
return fail(`Parameter ${flag} must be a number.`);
|
|
101
|
-
|
|
111
|
+
parsed = Number(value);
|
|
102
112
|
}
|
|
103
|
-
else if (
|
|
113
|
+
else if (type === 'boolean') {
|
|
104
114
|
if (!['true', 'false'].includes(value))
|
|
105
115
|
return fail(`Parameter ${flag} must be true or false.`);
|
|
106
|
-
|
|
116
|
+
parsed = value === 'true';
|
|
117
|
+
}
|
|
118
|
+
if (repeatable) {
|
|
119
|
+
const values = (params[key] ??= []);
|
|
120
|
+
values.push(parsed);
|
|
107
121
|
}
|
|
108
122
|
else
|
|
109
|
-
params[key] =
|
|
123
|
+
params[key] = parsed;
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
if (!params || Array.isArray(params) || typeof params !== 'object')
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Package-supplied schema patterns never run in the backtracking JS engine. */
|
|
2
|
+
export declare function schemaPattern(pattern: string): {
|
|
3
|
+
test(value: string): boolean;
|
|
4
|
+
toString(): string;
|
|
5
|
+
};
|
|
6
|
+
/** Ajv's pattern and patternProperties keywords must use the same engine. */
|
|
7
|
+
export declare const schemaRegExp: ((pattern: string, _flags: string) => {
|
|
8
|
+
test(value: string): boolean;
|
|
9
|
+
toString(): string;
|
|
10
|
+
}) & {
|
|
11
|
+
code: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RE2JS } from 're2js';
|
|
2
|
+
/** Package-supplied schema patterns never run in the backtracking JS engine. */
|
|
3
|
+
export function schemaPattern(pattern) {
|
|
4
|
+
if (pattern.length > 4096)
|
|
5
|
+
throw new Error('schema pattern exceeds 4096 characters');
|
|
6
|
+
const compiled = RE2JS.compile(RE2JS.translateRegExp(pattern));
|
|
7
|
+
return { test: value => compiled.test(value), toString: () => pattern };
|
|
8
|
+
}
|
|
9
|
+
/** Ajv's pattern and patternProperties keywords must use the same engine. */
|
|
10
|
+
export const schemaRegExp = Object.assign((pattern, _flags) => schemaPattern(pattern), { code: 'schemaRegExp' });
|
package/dist/ctl/redaction.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CtlError, ctlError } from './errors.js';
|
|
2
|
+
import { schemaPattern } from './pattern.js';
|
|
2
3
|
export const REDACTED = '<redacted>';
|
|
3
4
|
function reference(root, ref) {
|
|
4
5
|
if (!ref.startsWith('#'))
|
|
@@ -55,6 +56,22 @@ function branches(schemas, root) {
|
|
|
55
56
|
return result;
|
|
56
57
|
}
|
|
57
58
|
export function redactValue(value, schema, root = schema, onSecret) {
|
|
59
|
+
const patterns = new Map();
|
|
60
|
+
const matches = (pattern, key) => {
|
|
61
|
+
let compiled = patterns.get(pattern);
|
|
62
|
+
if (!compiled) {
|
|
63
|
+
// Cached descriptors and direct callers can bypass descriptor validation.
|
|
64
|
+
// Unsupported syntax must conservatively apply its privacy annotations.
|
|
65
|
+
try {
|
|
66
|
+
compiled = schemaPattern(pattern);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
compiled = { test: () => undefined };
|
|
70
|
+
}
|
|
71
|
+
patterns.set(pattern, compiled);
|
|
72
|
+
}
|
|
73
|
+
return compiled.test(key);
|
|
74
|
+
};
|
|
58
75
|
const walk = (input, schemas) => {
|
|
59
76
|
const nodes = branches(schemas, root);
|
|
60
77
|
if (nodes.some(node => node.writeOnly === true)) {
|
|
@@ -76,10 +93,12 @@ export function redactValue(value, schema, root = schema, onSecret) {
|
|
|
76
93
|
matched = true;
|
|
77
94
|
}
|
|
78
95
|
for (const [pattern, child] of Object.entries(node.patternProperties ?? {})) {
|
|
79
|
-
|
|
96
|
+
const match = matches(pattern, key);
|
|
97
|
+
if (match !== false)
|
|
80
98
|
children.push(child);
|
|
99
|
+
// An unknown match must not suppress additionalProperties privacy.
|
|
100
|
+
if (match === true)
|
|
81
101
|
matched = true;
|
|
82
|
-
}
|
|
83
102
|
}
|
|
84
103
|
if (!matched)
|
|
85
104
|
children.push(node.additionalProperties);
|
package/dist/ctl/service.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CtlError } from './errors.js';
|
|
1
2
|
import { type DsmSession, type JsonRequest } from './transport.js';
|
|
2
3
|
import { CtlStore, type Host } from './store.js';
|
|
3
4
|
import { type Descriptor } from './descriptor.js';
|
|
@@ -11,6 +12,18 @@ export interface DiscoverOptions {
|
|
|
11
12
|
version: string;
|
|
12
13
|
}[]) => Promise<boolean>;
|
|
13
14
|
}
|
|
15
|
+
interface InstalledApp {
|
|
16
|
+
id: string;
|
|
17
|
+
version: string;
|
|
18
|
+
aliases: string[];
|
|
19
|
+
error?: CtlError;
|
|
20
|
+
}
|
|
21
|
+
interface Discovery {
|
|
22
|
+
apps: Descriptor[];
|
|
23
|
+
installed: InstalledApp[];
|
|
24
|
+
diagnostics: any[];
|
|
25
|
+
cacheHits: number;
|
|
26
|
+
}
|
|
14
27
|
export declare class CtlClient {
|
|
15
28
|
store: CtlStore;
|
|
16
29
|
host: Host;
|
|
@@ -24,11 +37,9 @@ export declare class CtlClient {
|
|
|
24
37
|
version: string;
|
|
25
38
|
state?: unknown;
|
|
26
39
|
}[]>;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
cacheHits: number;
|
|
31
|
-
}>;
|
|
40
|
+
private needsApproval;
|
|
41
|
+
selectApp(listing: Discovery, name: string): Descriptor;
|
|
42
|
+
discover(options?: DiscoverOptions): Promise<Discovery>;
|
|
32
43
|
execute(name: string, path: string[], args: string[], options?: DiscoverOptions & {
|
|
33
44
|
yes?: boolean;
|
|
34
45
|
dryRun?: boolean;
|
|
@@ -39,3 +50,4 @@ export declare class CtlClient {
|
|
|
39
50
|
private executeWithin;
|
|
40
51
|
private appCookieSession;
|
|
41
52
|
}
|
|
53
|
+
export {};
|
package/dist/ctl/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CtlError,
|
|
1
|
+
import { CtlError, ctlError, errorData } from './errors.js';
|
|
2
2
|
import { APP_COOKIE, CTL_TIMEOUT_MS, createRequest } from './transport.js';
|
|
3
3
|
import { DESCRIPTION_METHOD, validateDescriptor, ajv } from './descriptor.js';
|
|
4
4
|
import { runFlow } from './flows.js';
|
|
@@ -41,24 +41,26 @@ export class CtlClient {
|
|
|
41
41
|
return entries.map((p) => { if (typeof p.id !== 'string' || !/^[A-Za-z][A-Za-z0-9_.+-]{0,127}$/.test(p.id) || typeof p.version !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._+~-]{0,63}$/.test(p.version))
|
|
42
42
|
throw new CtlError('CTL_INVENTORY_INVALID', 'Invalid package identity.', 'Check the DSM response.', 5); return { id: p.id, version: p.version, state: p.additional?.status }; });
|
|
43
43
|
}
|
|
44
|
+
needsApproval(pkg) {
|
|
45
|
+
return new CtlError('CTL_APP_NEEDS_APPROVAL', `${pkg.id} ${pkg.version} needs approval`, `run: synopkgland ctl list --app ${pkg.id} --approve-installed --host ${this.host.name}`, 4);
|
|
46
|
+
}
|
|
47
|
+
selectApp(listing, name) {
|
|
48
|
+
const pkg = selectApp(listing.installed, name);
|
|
49
|
+
if (pkg.error)
|
|
50
|
+
throw pkg.error;
|
|
51
|
+
return listing.apps.find(app => app.payload.package.id === pkg.id);
|
|
52
|
+
}
|
|
44
53
|
async discover(options = {}) {
|
|
45
54
|
const session = await this.store.session(this.host);
|
|
46
|
-
let packages = await this.inventory(session)
|
|
55
|
+
let packages = (await this.inventory(session)).map(pkg => {
|
|
56
|
+
const approved = this.host.approved[pkg.id];
|
|
57
|
+
// Previous-version metadata identifies aliases only; it never authorizes a call.
|
|
58
|
+
const description = this.store.cache(this.host, pkg.id, pkg.version)?.description ?? (approved ? this.store.cache(this.host, pkg.id, approved.version)?.description : undefined);
|
|
59
|
+
return { ...pkg, aliases: description?.payload.aliases ?? [] };
|
|
60
|
+
});
|
|
47
61
|
// An exact package filter narrows credential delivery. Alias selection needs the cached descriptors first.
|
|
48
|
-
if (options.app)
|
|
49
|
-
|
|
50
|
-
if (exact)
|
|
51
|
-
packages = [exact];
|
|
52
|
-
else {
|
|
53
|
-
const match = packages.find(p => this.store.cache(this.host, p.id, p.version)?.description?.payload.aliases?.some((a) => a.toLowerCase() === options.app.toLowerCase()));
|
|
54
|
-
if (match)
|
|
55
|
-
packages = [match];
|
|
56
|
-
else {
|
|
57
|
-
const known = packages.flatMap(p => { const d = this.store.cache(this.host, p.id, p.version)?.description; return d ? [p.id, ...d.payload.aliases] : []; });
|
|
58
|
-
choose(options.app, known, 'app');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
+
if (options.app)
|
|
63
|
+
packages = [selectApp(packages, options.app)];
|
|
62
64
|
const pending = packages.filter(p => this.host.approved[p.id]?.version !== p.version);
|
|
63
65
|
if (pending.length && (options.approve || await options.confirm?.(pending))) {
|
|
64
66
|
for (const p of pending)
|
|
@@ -68,11 +70,14 @@ export class CtlClient {
|
|
|
68
70
|
let cacheHits = 0;
|
|
69
71
|
const results = await boundedMap(packages, 4, async (pkg) => {
|
|
70
72
|
const approval = this.host.approved[pkg.id];
|
|
71
|
-
if (approval?.version !== pkg.version)
|
|
72
|
-
|
|
73
|
+
if (approval?.version !== pkg.version) {
|
|
74
|
+
pkg.error = this.needsApproval(pkg);
|
|
75
|
+
return { error: { package: pkg.id, ...errorData(pkg.error) } };
|
|
76
|
+
}
|
|
73
77
|
const cached = options.refresh ? null : this.store.cache(this.host, pkg.id, pkg.version);
|
|
74
78
|
if (cached?.failure) {
|
|
75
79
|
cacheHits++;
|
|
80
|
+
pkg.error = new CtlError(cached.failure.code, cached.failure.message, cached.failure.hint, 5);
|
|
76
81
|
return { error: { package: pkg.id, ...cached.failure, cached: true } };
|
|
77
82
|
}
|
|
78
83
|
try {
|
|
@@ -87,13 +92,19 @@ export class CtlClient {
|
|
|
87
92
|
throw new CtlError('CTL_DESCRIPTION_UNAVAILABLE', `Description refused (${String(response?.error?.code ?? 'invalid response')}).`, 'Use --refresh after updating the App.', 5);
|
|
88
93
|
description = validateDescriptor(response.data, pkg.id, pkg.version);
|
|
89
94
|
}
|
|
90
|
-
if (approval.hash && approval.hash !== description.sha256 && !options.approve)
|
|
91
|
-
|
|
95
|
+
if (approval.hash && approval.hash !== description.sha256 && !options.approve) {
|
|
96
|
+
// Remember the observed change so a later cache hit cannot revive the old approval.
|
|
97
|
+
this.store.putCache(this.host, pkg.id, pkg.version, description);
|
|
98
|
+
pkg.aliases = [...new Set([...pkg.aliases, ...description.payload.aliases])];
|
|
99
|
+
throw this.needsApproval(pkg);
|
|
100
|
+
}
|
|
101
|
+
pkg.aliases = description.payload.aliases;
|
|
92
102
|
approval.hash = description.sha256;
|
|
93
103
|
this.store.putCache(this.host, pkg.id, pkg.version, description);
|
|
94
104
|
return { description };
|
|
95
105
|
}
|
|
96
106
|
catch (error) {
|
|
107
|
+
pkg.error = ctlError(error);
|
|
97
108
|
const failure = errorData(error);
|
|
98
109
|
if (failure.code === 'CTL_DESCRIPTION_UNAVAILABLE')
|
|
99
110
|
this.store.putFailure(this.host, pkg.id, pkg.version, failure);
|
|
@@ -102,7 +113,7 @@ export class CtlClient {
|
|
|
102
113
|
});
|
|
103
114
|
this.host.lastUsed = new Date().toISOString();
|
|
104
115
|
this.store.save(this.host);
|
|
105
|
-
return { apps: results.flatMap(r => r.description ? [r.description] : []), diagnostics: results.flatMap(r => r.error ? [r.error] : []), cacheHits };
|
|
116
|
+
return { apps: results.flatMap(r => r.description ? [r.description] : []), installed: packages, diagnostics: results.flatMap(r => r.error ? [r.error] : []), cacheHits };
|
|
106
117
|
}
|
|
107
118
|
async execute(name, path, args, options = {}) {
|
|
108
119
|
try {
|
|
@@ -116,7 +127,7 @@ export class CtlClient {
|
|
|
116
127
|
const started = performance.now();
|
|
117
128
|
this.until = started + (options.timeoutMs ?? CTL_TIMEOUT_MS);
|
|
118
129
|
const listing = await this.discover(options);
|
|
119
|
-
const app = selectApp(listing
|
|
130
|
+
const app = this.selectApp(listing, name);
|
|
120
131
|
const { id, operation } = selectOperation(app, path);
|
|
121
132
|
const params = operationInput(operation, args, options.stdin, `run: synopkgland ctl ${name} ${operation.cli.join(' ')} --host ${this.host.name} --help`);
|
|
122
133
|
const output = new SensitiveOutput(params, operation.paramsSchema);
|
package/dist/e2e/app-open.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { parse } from 'yaml';
|
|
3
3
|
import { resolveAppLaunch } from '../contract/app-launch.js';
|
|
4
4
|
import { trustedAppEntryPath } from '../contract/web-launch.js';
|
|
5
|
+
import { readDaemonIngress } from '../standalone/daemon-ingress.js';
|
|
5
6
|
function safeEntry(entry) {
|
|
6
7
|
const url = new URL(entry, 'https://dsm.example');
|
|
7
8
|
if ((!entry.startsWith('/') && !/^https?:\/\//.test(entry)) || entry.startsWith('//') ||
|
|
@@ -25,7 +26,10 @@ export function deriveSmokeEntry(raw, entry) {
|
|
|
25
26
|
if (!launch)
|
|
26
27
|
throw new Error('Invalid app launch mode in manifest.');
|
|
27
28
|
const window = launch === 'dsm-window' && !raw.console && raw.ui !== false && !raw.compose && !raw.daemon?.ingress;
|
|
28
|
-
const
|
|
29
|
+
const ingress = readDaemonIngress(raw.daemon?.ingress, raw.package, () => { });
|
|
30
|
+
if (raw.daemon?.ingress !== undefined && !ingress)
|
|
31
|
+
throw new Error('Invalid daemon ingress: use /synopkgland/<package>/ with the exact package ID');
|
|
32
|
+
const derived = ingress?.path ?? (gateway
|
|
29
33
|
? `/webman/3rdparty/${raw.package}/sso/open.cgi`
|
|
30
34
|
: native ? `/?launchApp=${encodeURIComponent(appId)}`
|
|
31
35
|
: trustedAppEntryPath(raw));
|
package/dist/error-registry.js
CHANGED
|
@@ -336,7 +336,7 @@ export const ERROR_REGISTRY = {
|
|
|
336
336
|
STANDALONE_DAEMON_RESTART_SDK_REQUIRED: {
|
|
337
337
|
code: "STANDALONE_DAEMON_RESTART_SDK_REQUIRED",
|
|
338
338
|
description: "The selected controller SDK version or source does not support daemon.restart.",
|
|
339
|
-
suggestion: "Upgrade the selected controller SDK to 0.26.0 or later and use matching sources, including a local SDK override; update the App SDK requirement, identity_gateway.sdk or the
|
|
339
|
+
suggestion: "Upgrade the selected controller SDK to 0.26.0 or later and use matching sources, including a local SDK override; update the App SDK requirement, identity_gateway.sdk or the CLI that supplies the default.",
|
|
340
340
|
},
|
|
341
341
|
UNINSTALL_INVALID: {
|
|
342
342
|
code: "UNINSTALL_INVALID",
|
|
@@ -386,7 +386,7 @@ export const ERROR_REGISTRY = {
|
|
|
386
386
|
STANDALONE_DAEMON_INGRESS_INVALID: {
|
|
387
387
|
code: "STANDALONE_DAEMON_INGRESS_INVALID",
|
|
388
388
|
description: "daemon.ingress does not declare one same-origin path and a bounded proxy timeout.",
|
|
389
|
-
suggestion: "Set daemon.ingress.path to /<
|
|
389
|
+
suggestion: "Set daemon.ingress.path to exactly /synopkgland/<package>/ using the case-sensitive manifest package ID, and timeout (optional) to 1–3600 seconds",
|
|
390
390
|
},
|
|
391
391
|
STANDALONE_DAEMON_INGRESS_MIN_DSM: {
|
|
392
392
|
code: "STANDALONE_DAEMON_INGRESS_MIN_DSM",
|
|
@@ -12,6 +12,20 @@ export interface InstallOptions {
|
|
|
12
12
|
dryRun?: boolean;
|
|
13
13
|
}
|
|
14
14
|
export declare function installPackage(spec: string, options?: InstallOptions, connection?: NasConnection): Promise<{
|
|
15
|
+
message?: string | undefined;
|
|
16
|
+
host: string;
|
|
17
|
+
target: NasTarget;
|
|
18
|
+
source: string;
|
|
19
|
+
packages: {
|
|
20
|
+
signed?: boolean | undefined;
|
|
21
|
+
package: string;
|
|
22
|
+
version: string;
|
|
23
|
+
size: number;
|
|
24
|
+
action: string;
|
|
25
|
+
}[];
|
|
26
|
+
parameters: string[];
|
|
27
|
+
} | {
|
|
28
|
+
message?: string | undefined;
|
|
15
29
|
host: string;
|
|
16
30
|
target: NasTarget;
|
|
17
31
|
source: string;
|
|
@@ -26,6 +40,7 @@ export declare function installPackage(spec: string, options?: InstallOptions, c
|
|
|
26
40
|
dryRun: boolean;
|
|
27
41
|
} | {
|
|
28
42
|
status: import("./catalog-install.js").InstalledPackageState | undefined;
|
|
43
|
+
message?: string | undefined;
|
|
29
44
|
host: string;
|
|
30
45
|
target: NasTarget;
|
|
31
46
|
source: string;
|