@synopackageland/cli 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +13 -0
- package/dist/cli.js +9 -3
- package/dist/error-registry.js +1 -0
- package/dist/runtime/approval-diff.d.ts +37 -0
- package/dist/runtime/approval-diff.js +56 -0
- package/dist/runtime/executor-digests.json +15 -4
- package/dist/standalone/dsm-window.js +3 -3
- package/dist/standalone/identity-gateway.d.ts +1 -0
- package/dist/standalone/identity-gateway.js +3 -1
- package/dist/standalone/oidc-resume.js +12 -0
- package/dist/standalone/root-window.js +6 -1
- package/package.json +2 -2
- package/skills/SKILL.md +2 -0
- package/skills/reference/approval-upgrades.md +36 -0
- package/skills/reference/identity-gateway.md +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.1 (2026-10-20)
|
|
4
|
+
|
|
5
|
+
- Record SDK 0.19.1 executor and lifecycle digests for the executor fix that accepts `deadline_ms` on delegated `webapi.call`; SDK 0.19.0 digests remain. Executor Apps rebuilt with SDK 0.19.1 require one administrator reapproval after upgrading.
|
|
6
|
+
- Compare upgrade approval requirements with a baseline SPK: `build --baseline` and `inspect --baseline` report root program digests, policy differences, task template changes and executor-only handoff eligibility.
|
|
7
|
+
- Retain templates 0.19.0 (SDK 0.19.0) and UI 0.12.5.
|
|
8
|
+
|
|
9
|
+
## 0.19.0 (2026-10-20)
|
|
10
|
+
|
|
11
|
+
- Align the CLI SDK minor line and templates at 0.19.0; retain UI 0.12.5.
|
|
12
|
+
- Support native DSM OIDC login without a browser bridge for compatible clients.
|
|
13
|
+
- Refresh canonical executor setup fixtures and all three executor/lifecycle architecture pins for SDK 0.19.0. Rebuilt executor Apps require one administrator reapproval after upgrading.
|
|
14
|
+
|
|
3
15
|
## 0.18.0 (2026-10-20)
|
|
4
16
|
|
|
5
17
|
- Support the SDK 0.18.x minor line and pin templates 0.18.0 with SDK/App 0.18.0 and UI 0.12.5.
|
package/README.md
CHANGED
|
@@ -119,6 +119,19 @@ window's approval waits for it, while a status read answers `executor_starting`
|
|
|
119
119
|
SDK's panels check again on their own. The program cannot declare `run` or `job`, and
|
|
120
120
|
declaring it changes the policy digest, so an administrator approves the App again.
|
|
121
121
|
|
|
122
|
+
Before releasing an executor App, compare it with the previous released SPK of
|
|
123
|
+
that package and architecture. Include the resulting summary in release notes:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
synopkgland build --arch x86_64 --baseline previous.spk --json
|
|
127
|
+
synopkgland inspect current.spk --baseline previous.spk
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The summary covers root program digests, permission/parameter differences, task
|
|
131
|
+
template changes and executor-only handover eligibility. Without a comparable
|
|
132
|
+
baseline, approval retention is unknown. Eligibility still requires live protected
|
|
133
|
+
runtime and exact DSM task readback. See [the approval guide](skills/reference/approval-upgrades.md).
|
|
134
|
+
|
|
122
135
|
## Commands
|
|
123
136
|
|
|
124
137
|
| Command | Purpose |
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ import { buildNativeLayout, isNativeLayout, loadNativeLayout, LAYOUT_ARCHES, } f
|
|
|
33
33
|
import { buildStandalone, runnerFor } from "./standalone/build.js";
|
|
34
34
|
import { checkJobs, commitOutputs, forEachArch, } from "./standalone/arch-jobs.js";
|
|
35
35
|
import { checkReproducible } from "./reproducible.js";
|
|
36
|
+
import { spkApprovalDiff, formatApprovalDiff } from "./runtime/approval-diff.js";
|
|
36
37
|
import { inspectAnySpk } from "./spk/inspect.js";
|
|
37
38
|
function failure(code, message, hint = "Fix the configuration and retry; use --help to see command usage.") {
|
|
38
39
|
return Object.assign(new Error(codedMessage(code, message)), { code, hint });
|
|
@@ -389,16 +390,18 @@ export async function runCli(argv) {
|
|
|
389
390
|
});
|
|
390
391
|
program
|
|
391
392
|
.command("inspect <spk>")
|
|
393
|
+
.option("--baseline <spk>", "Compare approval requirements with the previous SPK")
|
|
392
394
|
.description("Inspect any SPK against the DSM 7 package format without installing; for a synopkgland-built SPK also check the build contract and root privilege requirements")
|
|
393
395
|
.option("--json", "Print a single JSON object")
|
|
394
|
-
.action(async (input) => {
|
|
396
|
+
.action(async (input, options) => {
|
|
395
397
|
const spk = resolve(input);
|
|
396
398
|
if (existsSync(spk) && statSync(spk).isDirectory())
|
|
397
399
|
throw failure("SPK_INSPECTION_FAILED", `${spk} is a directory; inspect checks .spk files.`, "Use synopkgland check to inspect project settings.");
|
|
398
400
|
const report = await inspectAnySpk(spk);
|
|
399
401
|
if (!report.dsm.ok || report.findings.length)
|
|
400
402
|
throw Object.assign(failure("SPK_INSPECTION_FAILED", formatSpkChecks(report).join("\n"), "Fix the SPK by error code and repackage; see reference/errors for the codes; do not install an SPK that fails inspection."), { report: { spk, ...report } });
|
|
401
|
-
|
|
403
|
+
const approval = await spkApprovalDiff(spk, options.baseline && resolve(options.baseline), report);
|
|
404
|
+
emit("inspect", { spk, ...report, approval }, formatSpkReport(spk, report) + "\n" + formatApprovalDiff(approval));
|
|
402
405
|
});
|
|
403
406
|
const images = program.command("images").description("Container image tools");
|
|
404
407
|
images
|
|
@@ -624,6 +627,7 @@ export async function runCli(argv) {
|
|
|
624
627
|
skill.command("show <id>").action((id) => stdout(readSkill(id)));
|
|
625
628
|
program
|
|
626
629
|
.command("build")
|
|
630
|
+
.option("--baseline <spk>", "Compare approval requirements with the previous SPK (one matching architecture)")
|
|
627
631
|
.description("Build an SPK from synology-app.yaml or dsm-package.yaml")
|
|
628
632
|
.option("--json", "Print a single JSON object")
|
|
629
633
|
.option("-o, --output <path>", "Output SPK; use with a single --arch")
|
|
@@ -684,13 +688,15 @@ export async function runCli(argv) {
|
|
|
684
688
|
outDir: undefined,
|
|
685
689
|
}),
|
|
686
690
|
});
|
|
691
|
+
const approval = await Promise.all(outputs.map(async (output) => ({ output, ...await spkApprovalDiff(output, options.baseline && resolve(options.baseline)) })));
|
|
687
692
|
const lines = [
|
|
693
|
+
...approval.map(item => item.output + "\n" + formatApprovalDiff(item)),
|
|
688
694
|
...outputs.map((output) => `Built ${output}`),
|
|
689
695
|
...(reproducible ? reproducibleLines(reproducible) : []),
|
|
690
696
|
];
|
|
691
697
|
if (reproducible && !reproducible.identical)
|
|
692
698
|
throw Object.assign(failure("BUILD_NOT_REPRODUCIBLE", lines.join("\n"), "Find the payload files that differ between builds (timestamps, random paths, unpinned dependencies) and retry after fixing them."), { report: { outputs, reproducible } });
|
|
693
|
-
emit("build", { outputs, ...(reproducible ? { reproducible } : {}) }, lines.join("\n"));
|
|
699
|
+
emit("build", { outputs, approval, ...(reproducible ? { reproducible } : {}) }, lines.join("\n"));
|
|
694
700
|
});
|
|
695
701
|
if (json)
|
|
696
702
|
console.log = (...args) => console.error(...args);
|
package/dist/error-registry.js
CHANGED
|
@@ -11,6 +11,7 @@ export const ERROR_REGISTRY = {
|
|
|
11
11
|
CTL_DESCRIBE_FAILED: { "code": "CTL_DESCRIBE_FAILED", "description": "The backend registry export failed.", "suggestion": "Check the backend --describe output and build diagnostics." },
|
|
12
12
|
CTL_COMMAND_UNREGISTERED: { "code": "CTL_COMMAND_UNREGISTERED", "description": "A command references an unregistered backend method.", "suggestion": "Correct ctl.commands bindings or register the backend method." },
|
|
13
13
|
CTL_DESCRIPTOR_INVALID: { "code": "CTL_DESCRIPTOR_INVALID", "description": "The generated API description violates the public contract.", "suggestion": "Correct ctl.commands and rebuild the private synopkgland-api.json." },
|
|
14
|
+
APPROVAL_BASELINE_INVALID: { code: "APPROVAL_BASELINE_INVALID", description: "The --baseline SPK failed inspection, so approval retention cannot be compared.", suggestion: "Pass the previous released SPK of the same package and architecture, unmodified; inspect it to see each finding." },
|
|
14
15
|
CTL_DECLARATION_INVALID: { "code": "CTL_DECLARATION_INVALID", "description": "The ctl declaration is invalid.", "suggestion": "Check ctl.enabled, aliases and explicit commands in dsm-package.yaml." },
|
|
15
16
|
...{
|
|
16
17
|
LAUNCHER_PORTAL_MISMATCH: {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ExecutorPolicy } from './executor-policy.js';
|
|
2
|
+
import { type SpkReport } from '../spk/inspect.js';
|
|
3
|
+
export interface ApprovalInventory {
|
|
4
|
+
package: string;
|
|
5
|
+
arch: string;
|
|
6
|
+
executorDigest: string;
|
|
7
|
+
policyDigest: string;
|
|
8
|
+
anchored: boolean;
|
|
9
|
+
policy: ExecutorPolicy;
|
|
10
|
+
tasks: Array<{
|
|
11
|
+
kind: string;
|
|
12
|
+
name: string;
|
|
13
|
+
script: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export interface ApprovalDifference {
|
|
17
|
+
retention: 'unknown' | 'eligible' | 'required';
|
|
18
|
+
permissions: 'unknown' | 'unchanged' | 'expanded' | 'review-required';
|
|
19
|
+
executor_only: boolean;
|
|
20
|
+
task_template_changed: boolean | null;
|
|
21
|
+
old_policy_digest?: string;
|
|
22
|
+
new_policy_digest?: string;
|
|
23
|
+
root_programs: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
old: string | null;
|
|
26
|
+
new: string | null;
|
|
27
|
+
}>;
|
|
28
|
+
policy_changes: Array<{
|
|
29
|
+
path: string;
|
|
30
|
+
old: unknown;
|
|
31
|
+
new: unknown;
|
|
32
|
+
}>;
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function approvalDiff(old: ApprovalInventory | undefined, next: ApprovalInventory | undefined): ApprovalDifference;
|
|
36
|
+
export declare function spkApprovalDiff(current: string, baseline?: string, report?: SpkReport): Promise<ApprovalDifference>;
|
|
37
|
+
export declare function formatApprovalDiff(d: ApprovalDifference): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { inspectAnySpk } from '../spk/inspect.js';
|
|
2
|
+
function canonical(value) {
|
|
3
|
+
if (Array.isArray(value))
|
|
4
|
+
return '[' + value.map(canonical).join(',') + ']';
|
|
5
|
+
if (value && typeof value === 'object')
|
|
6
|
+
return '{' + Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => JSON.stringify(k) + ':' + canonical(v)).join(',') + '}';
|
|
7
|
+
return JSON.stringify(value) ?? 'null';
|
|
8
|
+
}
|
|
9
|
+
function authority(policy) {
|
|
10
|
+
const p = structuredClone(policy);
|
|
11
|
+
p.root = Object.fromEntries(Object.entries(policy.root).map(([name, entry]) => [name, typeof entry === 'string' ? {} : Object.fromEntries(Object.entries(entry).filter(([key]) => key !== 'digest'))]));
|
|
12
|
+
return p;
|
|
13
|
+
}
|
|
14
|
+
function changes(a, b, path = '', out = []) {
|
|
15
|
+
if (canonical(a) === canonical(b))
|
|
16
|
+
return out;
|
|
17
|
+
if (a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
|
|
18
|
+
for (const key of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort())
|
|
19
|
+
changes(a[key], b[key], path + '/' + key.replaceAll('~', '~0').replaceAll('/', '~1'), out);
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
out.push({ path, old: a ?? null, new: b ?? null });
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
const rootDigests = (p) => Object.fromEntries(Object.entries(p.root).map(([name, e]) => [name, typeof e === 'string' ? e : e.digest]));
|
|
26
|
+
export function approvalDiff(old, next) {
|
|
27
|
+
const unknown = { retention: 'unknown', permissions: 'unknown', executor_only: false, task_template_changed: null, root_programs: [], policy_changes: [], reason: 'Cannot determine whether approval can be retained: a valid, matching baseline SPK and executor inventory are required.' };
|
|
28
|
+
if (!old || !next || old.package !== next.package || old.arch !== next.arch)
|
|
29
|
+
return unknown;
|
|
30
|
+
const a = rootDigests(old.policy), b = rootDigests(next.policy);
|
|
31
|
+
const root_programs = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort().filter(name => a[name] !== b[name]).map(name => ({ name, old: a[name] ?? null, new: b[name] ?? null }));
|
|
32
|
+
const policy_changes = changes(authority(old.policy), authority(next.policy));
|
|
33
|
+
const expanded = next.policy.operations.some(op => !old.policy.operations.includes(op)) || Object.keys(next.policy.root).some(name => !(name in old.policy.root));
|
|
34
|
+
const templates = (i) => i.tasks.map(t => ({ kind: t.kind, name: t.name, script: t.script.replaceAll(i.policyDigest, '@POLICY_DIGEST@') })).sort((a, b) => a.kind < b.kind ? -1 : 1);
|
|
35
|
+
const task_template_changed = canonical(templates(old)) !== canonical(templates(next));
|
|
36
|
+
const eligible = old.policyDigest === next.policyDigest && !task_template_changed && (old.executorDigest === next.executorDigest || next.anchored);
|
|
37
|
+
return { retention: eligible ? 'eligible' : 'required', permissions: expanded ? 'expanded' : policy_changes.length ? 'review-required' : 'unchanged', executor_only: eligible && old.executorDigest !== next.executorDigest, task_template_changed, old_policy_digest: old.policyDigest, new_policy_digest: next.policyDigest, root_programs, policy_changes, reason: eligible ? 'Approval can be retained if the installed protected runtime and exact DSM task readback still match this baseline.' : 'Administrator reapproval is required for the changed payload, policy, executor anchor or task definitions.' };
|
|
38
|
+
}
|
|
39
|
+
function inventory(r) {
|
|
40
|
+
if (!r.dsm.ok || r.findings.length || !r.executor || !r.package || !r.arch)
|
|
41
|
+
return;
|
|
42
|
+
return { package: r.package, arch: r.arch, ...r.executor, anchored: true }; // inspect verifies the INFO anchor and every root payload digest.
|
|
43
|
+
}
|
|
44
|
+
export async function spkApprovalDiff(current, baseline, report) {
|
|
45
|
+
if (!baseline)
|
|
46
|
+
return approvalDiff(undefined, undefined);
|
|
47
|
+
const next = inventory(report ?? await inspectAnySpk(current));
|
|
48
|
+
const old = await inspectAnySpk(baseline);
|
|
49
|
+
if (!old.dsm.ok || old.findings.length)
|
|
50
|
+
throw new Error('[APPROVAL_BASELINE_INVALID] Baseline SPK failed inspection: ' + [...old.findings, ...old.dsm.errors.map(e => e.message)].join('; '));
|
|
51
|
+
return approvalDiff(inventory(old), next);
|
|
52
|
+
}
|
|
53
|
+
export function formatApprovalDiff(d) {
|
|
54
|
+
const short = (s) => s?.slice(0, 12) ?? '—';
|
|
55
|
+
return ['Approval: ' + d.reason, 'Permissions: ' + d.permissions, ...d.root_programs.map(p => `Root program ${p.name}: ${short(p.old)} → ${short(p.new)}`), ...d.policy_changes.map(c => `Policy ${c.path}: ${JSON.stringify(c.old)} → ${JSON.stringify(c.new)}`), `Task template: ${d.task_template_changed === null ? 'unknown' : d.task_template_changed ? 'changed' : 'unchanged'}`, `Executor-only handover: ${d.executor_only ? 'eligible under the INFO anchor' : 'not established'}`].join('\n');
|
|
56
|
+
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"0.
|
|
3
|
-
"x86_64": "
|
|
4
|
-
"aarch64": "
|
|
5
|
-
"armv7": "
|
|
2
|
+
"0.19.0": {
|
|
3
|
+
"x86_64": "3b23252a4d2ad188e8920aee3c022d054a85f31797dfb9f34a576449f8b08f41",
|
|
4
|
+
"aarch64": "024a8a01977324645b475330371afb8b8c8387eac6a11ec3d9d30a4a0e445174",
|
|
5
|
+
"armv7": "fae3266f65863815f1417332a3c73d12507941029077e0ddc118da8461e8cff3",
|
|
6
|
+
"lifecycle": {
|
|
7
|
+
"x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972",
|
|
8
|
+
"aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb",
|
|
9
|
+
"armv7": "256fb348b275da7ac5728a3cb7cb7c65660de0d5c2c59430ab4f5ade1b0a3681"
|
|
10
|
+
},
|
|
11
|
+
"goToolchain": "go1.26.9"
|
|
12
|
+
},
|
|
13
|
+
"0.19.1": {
|
|
14
|
+
"x86_64": "e27cfeb85fed221b06f9c58166f02c0f701a724308cd17c96a43a843a0b5c4aa",
|
|
15
|
+
"aarch64": "908db00cbd37d072f6ffc339e7bca24da5efb8cddd4befcbad02c7398beb7191",
|
|
16
|
+
"armv7": "a0f35d62ec80a1bf3fb49f5e2d1a5f7576eda569a6fd0695084cec6cb8e100bc",
|
|
6
17
|
"lifecycle": {
|
|
7
18
|
"x86_64": "9bb53301670b43cdcd6e21cebac99ab7abd82962d874f32423a81e6cb49d4972",
|
|
8
19
|
"aarch64": "38fe873c86eb102c850baa1f304114a83a31d1df72defd22792bc37610fec9bb",
|
|
@@ -379,7 +379,7 @@ ${ns}.prepareFullPage = function (title) {
|
|
|
379
379
|
favicon.href = ${ns}.appIcon;
|
|
380
380
|
};
|
|
381
381
|
|
|
382
|
-
${manifest.identityGateway?.oidc?.browser ? `${ns}.resumeOIDC = ${oidcResumeRuntime(pkg, Object.values(manifest.identityGateway.listen).filter((p) => typeof p === "number"))};` : ""}
|
|
382
|
+
${(manifest.identityGateway?.oidc?.native_login || manifest.identityGateway?.oidc?.browser) ? `${ns}.resumeOIDC = ${oidcResumeRuntime(pkg, Object.values(manifest.identityGateway.listen).filter((p) => typeof p === "number"))};` : ""}
|
|
383
383
|
|
|
384
384
|
// DSM reopens a window at the size it was last left at, even one saved on a
|
|
385
385
|
// larger screen, and only moves a window that does not fit: keep it inside.
|
|
@@ -408,7 +408,7 @@ Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
|
|
|
408
408
|
},
|
|
409
409
|
onOpen: function (param) {
|
|
410
410
|
this.callParent(arguments);
|
|
411
|
-
${manifest.identityGateway?.oidc?.browser ? `if (${ns}.resumeOIDC(param)) return;` : ""}
|
|
411
|
+
${(manifest.identityGateway?.oidc?.native_login || manifest.identityGateway?.oidc?.browser) ? `if (${ns}.resumeOIDC(param)) return;` : ""}
|
|
412
412
|
var me = this;
|
|
413
413
|
if (me.mounted) {
|
|
414
414
|
me.reopenEvents.emit();
|
|
@@ -471,7 +471,7 @@ ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
|
|
|
471
471
|
// with that launch's param. Before the App mounts, it mounts with the latest.
|
|
472
472
|
onRequest: function (param) {
|
|
473
473
|
this.callParent(arguments);
|
|
474
|
-
${manifest.identityGateway?.oidc?.browser ? `if (${ns}.resumeOIDC(param)) return;` : ""}
|
|
474
|
+
${(manifest.identityGateway?.oidc?.native_login || manifest.identityGateway?.oidc?.browser) ? `if (${ns}.resumeOIDC(param)) return;` : ""}
|
|
475
475
|
var params = ${ns}.launchParams(param);
|
|
476
476
|
if (!this.mounted) {
|
|
477
477
|
if (this.mountPending && Object.keys(params).length) this.launchParams = params;
|
|
@@ -55,6 +55,7 @@ export interface IdentityGateway {
|
|
|
55
55
|
roles?: GatewayRoles;
|
|
56
56
|
headers?: Partial<Record<"user" | "email" | "name" | "groups" | "uid", string>>;
|
|
57
57
|
oidc?: {
|
|
58
|
+
native_login?: boolean;
|
|
58
59
|
issuer_port: number;
|
|
59
60
|
browser?: GatewayBrowser;
|
|
60
61
|
clients: GatewayClient[];
|
|
@@ -270,7 +270,8 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
|
|
|
270
270
|
if (v.mode === "oidc") {
|
|
271
271
|
const o = v.oidc;
|
|
272
272
|
if (!record(o) ||
|
|
273
|
-
!only(o, ["issuer_port", "clients", "rewrite_json", "browser"]) ||
|
|
273
|
+
!only(o, ["issuer_port", "clients", "rewrite_json", "browser", "native_login"]) ||
|
|
274
|
+
(o.native_login !== undefined && typeof o.native_login !== "boolean") ||
|
|
274
275
|
!port(o.issuer_port) ||
|
|
275
276
|
!Array.isArray(o.clients) ||
|
|
276
277
|
!o.clients.length ||
|
|
@@ -312,6 +313,7 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
|
|
|
312
313
|
return invalid("oidc.browser needs fixed local start, login_paths and callback_paths outside /_sso.");
|
|
313
314
|
}
|
|
314
315
|
gateway.oidc = {
|
|
316
|
+
...(o.native_login !== undefined ? { native_login: o.native_login } : {}),
|
|
315
317
|
issuer_port: o.issuer_port,
|
|
316
318
|
...(b !== undefined ? { browser: b } : {}),
|
|
317
319
|
clients: clients,
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
/** No caller-provided URL reaches location: only a transaction and a declared port. */
|
|
2
2
|
export function oidcResumeRuntime(pkg, ports) {
|
|
3
3
|
return `(function(param) {
|
|
4
|
+
if (typeof param === 'string') {
|
|
5
|
+
if (param.length > 512) return false;
|
|
6
|
+
try { param = JSON.parse(param); }
|
|
7
|
+
catch (_) {
|
|
8
|
+
try {
|
|
9
|
+
var query = new URLSearchParams(param);
|
|
10
|
+
if (Array.from(query.keys()).length !== 1 || query.getAll('ssoResume').length !== 1) return false;
|
|
11
|
+
param = {ssoResume:query.get('ssoResume')};
|
|
12
|
+
} catch (_) { return false; }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (!param || typeof param !== 'object' || Array.isArray(param)) return false;
|
|
4
16
|
var value = param && param.ssoResume;
|
|
5
17
|
if (typeof value !== 'string') return false;
|
|
6
18
|
var parts = /^([A-Za-z0-9_-]{1,128}):([0-9]{1,5})$/.exec(value);
|
|
@@ -359,9 +359,14 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
|
359
359
|
// here, and that first status serves the health check. Approval and
|
|
360
360
|
// control read back on their own.
|
|
361
361
|
function checked(status) { return pinned(status) && !!status.tasks && status.tasks.state === "ready"; }
|
|
362
|
+
function approvalExplanation(verified, status) {
|
|
363
|
+
var diff = status && status.approval;
|
|
364
|
+
if (verified.state === "needs-admin-approval" && diff && options.pins && diff.package === options.pins.package && diff.new_policy_digest === options.pins.policyDigest && diff.new_executor_digest === options.pins.executorDigest && (diff.old_policy_digest !== diff.new_policy_digest || diff.task_template_changed)) verified.approval = diff;
|
|
365
|
+
return verified;
|
|
366
|
+
}
|
|
362
367
|
function adminStatus() {
|
|
363
368
|
var asked = options.call("executor.status", {});
|
|
364
|
-
function verify() { return read().then(function (verified) { return health(verified, 0, asked); }); }
|
|
369
|
+
function verify() { return read().then(function (verified) { return health(verified, 0, asked); }).then(function (verified) { return asked.then(function (status) { return approvalExplanation(verified, status); }, function () { return verified; }); }); }
|
|
365
370
|
return asked.then(function (status) {
|
|
366
371
|
return checked(status) ? {state: "ready", tasks: {boot: {state: "ready"}, control: {state: "ready"}}, executor: status} : verify();
|
|
367
372
|
}, verify);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@napi-rs/keyring": "^2.1.1",
|
|
30
|
-
"@synopackageland/templates": "0.
|
|
30
|
+
"@synopackageland/templates": "0.19.0",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|
package/skills/SKILL.md
CHANGED
|
@@ -5,6 +5,8 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
|
|
|
5
5
|
|
|
6
6
|
# Independent Synology SPKs
|
|
7
7
|
|
|
8
|
+
Before releasing an executor App, read [reference/approval-upgrades.md](reference/approval-upgrades.md). Run build/inspect with the previous released SPK as `--baseline` and include the approval difference in release notes. Without a comparable baseline, retention is unknown.
|
|
9
|
+
|
|
8
10
|
Use this guide to initialize, build and inspect own applications or upstream OSS as independent packages. 依已實作能力區分 DSM session 檔案、私有 jobs 與 App 登入;delegate 已實作並隨 CLI 提供固定操作的離線委託(預設 `files.moveFromStaging`);共用 delegate/rootOps executor 的完整裝置生命週期驗收仍待完成,不得宣稱已通過;支援邊界依 delegate guide/support matrix,禁止發明新的身分平台。
|
|
9
11
|
|
|
10
12
|
For an existing open-source App, first read [reference/upstream-fork.md](reference/upstream-fork.md): upstream fork/history + untouched upstream files + all packaging in `synology/`, official container images pinned by tag/index digest, native official release binaries pinned by SHA-256 or a source build hook, and a single tag-merge upgrade command. Use `init --upstream-fork`; never rewrite the App as a component. Integrated upstream Apps require direct DSM administrator entry without a separate App password or an OIDC/SSO server: supported trusted headers first, disabling native login second; the shipped OpenList pattern uses the existing SDK 0.17.3 token adapter (`mode: token`, `access: dsm-admins`, `entry: admin-only`) with server-side Authorization injection. All DSM admins share one upstream admin; token reset requires package restart; WebDAV retains its native credentials.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Approval changes before release
|
|
2
|
+
|
|
3
|
+
Run the current CLI against the previous released SPK for the same package and
|
|
4
|
+
architecture before releasing an executor App:
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
synopkgland build --arch x86_64 --baseline previous-x86_64.spk --json
|
|
8
|
+
synopkgland inspect current-x86_64.spk --baseline previous-x86_64.spk
|
|
9
|
+
synopkgland inspect current-x86_64.spk --baseline previous-x86_64.spk --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`inspect` returns `data.approval`; `build` returns one entry per output in
|
|
13
|
+
`data.approval`. Put the human summary in release notes. Optionally copy that
|
|
14
|
+
same summary into the manifest's `metadata.changelog`, which the existing build writes to INFO for Package Center.
|
|
15
|
+
Do not assume DSM automatically opens an App after upgrading it.
|
|
16
|
+
|
|
17
|
+
The summary names root programs and their old/new SHA-256 prefixes, policy
|
|
18
|
+
permission/parameter paths, and task template changes. Task template comparison
|
|
19
|
+
removes only the policy pin; a policy pin change still requires approval.
|
|
20
|
+
`unchanged` means the policy excluding root digests is identical. Added root
|
|
21
|
+
programs or operations are `expanded`; other permission/parameter changes are
|
|
22
|
+
`review-required`, because arbitrary schema and scope changes cannot generally
|
|
23
|
+
be proven to narrow permissions. Program bytes can change behavior even when
|
|
24
|
+
the declared policy is unchanged.
|
|
25
|
+
|
|
26
|
+
`eligible` is conditional: the live protected runtime must still match the
|
|
27
|
+
baseline and DSM must read back the exact approved tasks. An executor-only
|
|
28
|
+
handover additionally needs the new executor's valid INFO anchor. This offline
|
|
29
|
+
summary does not grant approval. With no baseline, an incompatible package or
|
|
30
|
+
architecture, or no executor inventory, retention is `unknown`; never promise
|
|
31
|
+
that approval will be retained. Invalid baselines fail inspection.
|
|
32
|
+
|
|
33
|
+
The SDK approval screen reads root-produced diagnostics through executor.status,
|
|
34
|
+
then uses the existing PasswordConfirm and exact task readback. Older protected
|
|
35
|
+
runtimes without diagnostic support show an explicit unknown explanation. No App
|
|
36
|
+
option supplies an approved baseline or permission claim.
|
|
@@ -121,3 +121,14 @@ URL is accepted as the return destination. The transaction expires in five minut
|
|
|
121
121
|
|
|
122
122
|
This declares navigation only; upstream account provisioning/role mapping must
|
|
123
123
|
use that upstream's supported configuration APIs and be tested independently.
|
|
124
|
+
|
|
125
|
+
## Server-managed OIDC navigation (development)
|
|
126
|
+
|
|
127
|
+
For an upstream that automatically starts OIDC (for example a native web-server
|
|
128
|
+
authentication module), leave `oidc.browser` unset and declare
|
|
129
|
+
`oidc.native_login: true`. This reuses native DSM cold-login/resume without
|
|
130
|
+
injecting an HTML/SPA bridge into the upstream. It requires the matching SDK
|
|
131
|
+
capability; released SDK 0.18.0 does not yet implement this field. Native Compose
|
|
132
|
+
URL entries resume only a bounded transaction and declared public gateway port.
|
|
133
|
+
Normal menu entry remains the classless SDK CGI. Existing browser consumers keep
|
|
134
|
+
their previous behavior. Never invent fake login paths just to enable DSM login.
|