@synopackageland/cli 0.27.1 → 0.29.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 +12 -0
- package/README.md +12 -2
- package/dist/error-registry.js +10 -0
- package/dist/standalone/compose-errors.js +3 -3
- package/dist/standalone/compose.js +2 -2
- package/dist/standalone/identity-gateway.d.ts +12 -0
- package/dist/standalone/identity-gateway.js +43 -1
- package/dist/standalone/inspect-spk.js +36 -3
- package/dist/standalone/oidc-resume.d.ts +1 -1
- package/dist/standalone/oidc-resume.js +11 -6
- package/dist/standalone/spk.js +5 -4
- package/package.json +2 -2
- package/skills/reference/compose.md +20 -5
- package/skills/reference/identity-gateway.md +62 -0
- package/skills/reference/lifecycle.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.29.0 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
- Depend on templates `~0.29.0` with SDK/App 0.29.0 and UI 0.14.10. SDK 0.29.0 reuses executor/v0.27.0 through executor manifest format 1; no executor digests change.
|
|
6
|
+
- Package `identity_gateway.external_access` (`connect: optional | none`, `origin_change: none | confirm`) for SDK 0.29.0 or later gateways; check refuses it for older SDKs and inspect verifies the packaged external declaration and `settings.cgi` gateway entry.
|
|
7
|
+
|
|
8
|
+
## 0.28.0 (2026-10-24)
|
|
9
|
+
|
|
10
|
+
- Depend on templates `~0.28.0` with SDK/App 0.28.0 and UI 0.14.10. SDK 0.28.0 reuses executor/v0.27.0 through executor manifest format 1; no executor digests change.
|
|
11
|
+
- Default the Compose startup budget to 900 seconds and document slow-NAS readiness; the SDK owns health timing and the Starting status. No per-App option is added.
|
|
12
|
+
- Keep UI CGI scripts executable in built SPKs, and let external OIDC resume tokens reach only the fixed DSM CGI.
|
|
13
|
+
- The release gate checks SDK 0.27.1+ executor builds against the SDK executor manifest instead of requiring a frozen legacy table entry.
|
|
14
|
+
|
|
3
15
|
## 0.27.1 (2026-10-23)
|
|
4
16
|
|
|
5
17
|
- Read executor release mappings, toolchain and six payload pins from SDK executor manifest format 1. Accept compatible SDK minors independently of CLI versions; keep the frozen 0.26.0/0.27.0 fallback. Remove the CLI digest-recording script.
|
package/README.md
CHANGED
|
@@ -66,8 +66,8 @@ CLI-only patch releases can differ from the SDK patch. Compose import and SDK de
|
|
|
66
66
|
read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
|
|
67
67
|
|
|
68
68
|
`init` and `add` read their templates from the locally installed
|
|
69
|
-
`@synopackageland/templates`. CLI 0.
|
|
70
|
-
update can select the latest templates patch in the 0.
|
|
69
|
+
`@synopackageland/templates`. CLI 0.29.0 depends on `~0.29.0`: npm installation or
|
|
70
|
+
update can select the latest templates patch in the 0.29 minor line. Templates
|
|
71
71
|
patches need only a templates release; update the npm installation to receive them.
|
|
72
72
|
The CLI never fetches templates at runtime, so offline use works after installation;
|
|
73
73
|
commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
|
|
@@ -303,6 +303,16 @@ or `portal`, or with `capabilities.rootOps`, `delegate`, `webapi` or
|
|
|
303
303
|
`identity`, whose approval or sign-in is shown in the SDK window
|
|
304
304
|
(`STANDALONE_UI_NO_WINDOW_INVALID`). A missing `ui` is still an error.
|
|
305
305
|
|
|
306
|
+
Gateway Apps built with SDK 0.29.0 or later include DSM-origin `sso/settings.cgi`
|
|
307
|
+
and default to `external_access: {connect: optional, origin_change: none}`.
|
|
308
|
+
Declare `identity_gateway.external_access.connect: none` to ignore Connect's
|
|
309
|
+
public URL projection; manual URLs remain available without Connect. Use
|
|
310
|
+
`origin_change: confirm` for Apps that need confirmation when their public
|
|
311
|
+
origin changes. `connect: required` is rejected, and declaring `external_access`
|
|
312
|
+
with SDK older than 0.29.0 is rejected. Compose and gateway SDK pins must match.
|
|
313
|
+
See [external access configuration](skills/reference/identity-gateway.md#external-access)
|
|
314
|
+
for the manifest, settings entry, URL rules and cold-login handoff.
|
|
315
|
+
|
|
306
316
|
Compose uses schema-2 `dsm-package.yaml` and `ui.launch`; the default is new-page.
|
|
307
317
|
Classless gateways use type url via the shared contract/app-launch resolver;
|
|
308
318
|
AppWindow wrappers use standalone. Headless projects have no launcher.
|
package/dist/error-registry.js
CHANGED
|
@@ -362,6 +362,16 @@ export const ERROR_REGISTRY = {
|
|
|
362
362
|
description: "The identity_gateway declaration does not match the gateway contract.",
|
|
363
363
|
suggestion: "Declare a daemon; set sdk, mode (oidc, header or token), the App's loopback upstream port and distinct listen ports other than 5000/5001; keep passthrough and redirects local paths outside /_sso; oidc needs issuer_port and clients, header needs headers.user, token needs token.exec in the daemon payload.",
|
|
364
364
|
},
|
|
365
|
+
STANDALONE_IDENTITY_GATEWAY_EXTERNAL_INVALID: {
|
|
366
|
+
code: "STANDALONE_IDENTITY_GATEWAY_EXTERNAL_INVALID",
|
|
367
|
+
description: "The external access declaration does not match the gateway contract.",
|
|
368
|
+
suggestion: "Set identity_gateway.external_access.connect to optional or none, and origin_change to none or confirm; required is not supported.",
|
|
369
|
+
},
|
|
370
|
+
STANDALONE_IDENTITY_GATEWAY_EXTERNAL_SDK_REQUIRED: {
|
|
371
|
+
code: "STANDALONE_IDENTITY_GATEWAY_EXTERNAL_SDK_REQUIRED",
|
|
372
|
+
description: "External access requires SDK 0.29.0 or later and matching gateway sources.",
|
|
373
|
+
suggestion: "Update identity_gateway.sdk and compose.sdk together to SDK 0.29.0 or later; ensure any development SDK override includes external access and settings.cgi support.",
|
|
374
|
+
},
|
|
365
375
|
STANDALONE_IDENTITY_GATEWAY_BUILD_FAILED: {
|
|
366
376
|
code: "STANDALONE_IDENTITY_GATEWAY_BUILD_FAILED",
|
|
367
377
|
description: "The SDK identity gateway could not be built from identity_gateway.sdk.",
|
|
@@ -13,7 +13,7 @@ export const COMPOSE_ERRORS = {
|
|
|
13
13
|
COMPOSE_STARTUP_INVALID: {
|
|
14
14
|
code: "COMPOSE_STARTUP_INVALID",
|
|
15
15
|
description: "The container startup budget is invalid.",
|
|
16
|
-
suggestion: "Set compose.startup_timeout_seconds to an integer from 2 to 900 seconds; the default is
|
|
16
|
+
suggestion: "Set compose.startup_timeout_seconds to an integer from 2 to 900 seconds; the default is 900, which a loaded HDD NAS may need. Read the same SYNOPKGLAND_STARTUP_BUDGET_SECONDS in the App bootstrap adapter.",
|
|
17
17
|
},
|
|
18
18
|
COMPOSE_STARTUP_TIMEOUT: {
|
|
19
19
|
code: "COMPOSE_STARTUP_TIMEOUT",
|
|
@@ -82,8 +82,8 @@ export const COMPOSE_ERRORS = {
|
|
|
82
82
|
},
|
|
83
83
|
COMPOSE_HEALTH_NOT_READY: {
|
|
84
84
|
code: "COMPOSE_HEALTH_NOT_READY",
|
|
85
|
-
description: "The App health endpoint did not return the expected status.",
|
|
86
|
-
suggestion: "Check the declared App health path/status and upstream health; redirects are not followed.",
|
|
85
|
+
description: "The App health endpoint did not return the expected status within the startup budget.",
|
|
86
|
+
suggestion: "Check the declared App health path/status and upstream health; redirects are not followed. daemon/daemon.log records how long the App stayed unavailable. Keep the default startup budget on slow NAS devices.",
|
|
87
87
|
},
|
|
88
88
|
COMPOSE_REQUEST_INVALID: {
|
|
89
89
|
code: "COMPOSE_REQUEST_INVALID",
|
|
@@ -77,7 +77,7 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
|
|
|
77
77
|
if (value.startup_timeout_seconds !== undefined &&
|
|
78
78
|
(!Number.isInteger(value.startup_timeout_seconds) ||
|
|
79
79
|
value.startup_timeout_seconds < 2 || value.startup_timeout_seconds > 900))
|
|
80
|
-
return invalid("compose.startup_timeout_seconds must be an integer from 2 to 900 seconds (default
|
|
80
|
+
return invalid("compose.startup_timeout_seconds must be an integer from 2 to 900 seconds (default 900).", "COMPOSE_STARTUP_INVALID");
|
|
81
81
|
let hostUdp;
|
|
82
82
|
if (value.host_udp !== undefined) {
|
|
83
83
|
const ports = new Set();
|
|
@@ -178,7 +178,7 @@ export function composeDaemon(m) {
|
|
|
178
178
|
args: ["run"],
|
|
179
179
|
targets: m.compose.targets,
|
|
180
180
|
env: {},
|
|
181
|
-
timeouts: { stop: 30, status: 5, uninstall: 60, ...m.lifecycleTimeouts, start: m.compose.plan.startup_timeout_seconds ?? m.lifecycleTimeouts?.start ??
|
|
181
|
+
timeouts: { stop: 30, status: 5, uninstall: 60, ...m.lifecycleTimeouts, start: m.compose.plan.startup_timeout_seconds ?? m.lifecycleTimeouts?.start ?? 900 },
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
184
|
export function composeNetwork(m) {
|
|
@@ -7,11 +7,22 @@ import type { StandaloneError, StandaloneManifest } from "./manifest.js";
|
|
|
7
7
|
export declare const GATEWAY_MAIN = "./idgateway/cmd/gateway";
|
|
8
8
|
/** Served by DSM at /webman/3rdparty/<package>/sso/authorize.cgi. */
|
|
9
9
|
export declare const GATEWAY_CGI_PATH = "ui/sso/authorize.cgi";
|
|
10
|
+
export declare const GATEWAY_SETTINGS_CGI_PATH = "ui/sso/settings.cgi";
|
|
10
11
|
/** The gateway inside the daemon payload; the App's payload must leave _sso/ free. */
|
|
11
12
|
export declare const GATEWAY_PAYLOAD_DIR = "_sso";
|
|
12
13
|
export declare const GATEWAY_EXEC = "_sso/gateway";
|
|
13
14
|
export declare const GATEWAY_CONFIG_PATH = "sso/gateway.json";
|
|
14
15
|
export type GatewayMode = "none" | "oidc" | "header" | "token";
|
|
16
|
+
export interface GatewayExternalAccess {
|
|
17
|
+
connect: "optional" | "none";
|
|
18
|
+
origin_change: "none" | "confirm";
|
|
19
|
+
}
|
|
20
|
+
export declare const EXTERNAL_ACCESS_MIN_SDK = "0.29.0";
|
|
21
|
+
/** SDK 0.28 has the diagnostic ping only; older gateways reject external JSON. */
|
|
22
|
+
export declare function externalAccessSdkSupported(sdk: string): boolean;
|
|
23
|
+
export declare function gatewayExternalAccess(gateway: IdentityGateway): GatewayExternalAccess | undefined;
|
|
24
|
+
/** The SDK executable dispatches these entry points by SCRIPT_NAME. */
|
|
25
|
+
export declare function gatewayCgiPaths(gateway: IdentityGateway): string[];
|
|
15
26
|
export interface GatewayRoles {
|
|
16
27
|
claim: string;
|
|
17
28
|
admin: string;
|
|
@@ -34,6 +45,7 @@ export interface GatewayBrowser {
|
|
|
34
45
|
export interface IdentityGateway {
|
|
35
46
|
/** The SDK release the gateway is built from. */
|
|
36
47
|
sdk: string;
|
|
48
|
+
external_access?: GatewayExternalAccess;
|
|
37
49
|
mode: GatewayMode;
|
|
38
50
|
/** Fixed same-origin menu target, including optional query and fragment. */
|
|
39
51
|
launch_path?: string;
|
|
@@ -13,10 +13,25 @@ import { StandaloneBuildError } from "./spk.js";
|
|
|
13
13
|
export const GATEWAY_MAIN = "./idgateway/cmd/gateway";
|
|
14
14
|
/** Served by DSM at /webman/3rdparty/<package>/sso/authorize.cgi. */
|
|
15
15
|
export const GATEWAY_CGI_PATH = "ui/sso/authorize.cgi";
|
|
16
|
+
export const GATEWAY_SETTINGS_CGI_PATH = "ui/sso/settings.cgi";
|
|
16
17
|
/** The gateway inside the daemon payload; the App's payload must leave _sso/ free. */
|
|
17
18
|
export const GATEWAY_PAYLOAD_DIR = "_sso";
|
|
18
19
|
export const GATEWAY_EXEC = `${GATEWAY_PAYLOAD_DIR}/gateway`;
|
|
19
20
|
export const GATEWAY_CONFIG_PATH = "sso/gateway.json";
|
|
21
|
+
export const EXTERNAL_ACCESS_MIN_SDK = "0.29.0";
|
|
22
|
+
/** SDK 0.28 has the diagnostic ping only; older gateways reject external JSON. */
|
|
23
|
+
export function externalAccessSdkSupported(sdk) {
|
|
24
|
+
const [major, minor] = sdk.split(".").map(Number);
|
|
25
|
+
return major > 0 || minor >= 29;
|
|
26
|
+
}
|
|
27
|
+
export function gatewayExternalAccess(gateway) {
|
|
28
|
+
return gateway.external_access ?? (externalAccessSdkSupported(gateway.sdk)
|
|
29
|
+
? { connect: "optional", origin_change: "none" } : undefined);
|
|
30
|
+
}
|
|
31
|
+
/** The SDK executable dispatches these entry points by SCRIPT_NAME. */
|
|
32
|
+
export function gatewayCgiPaths(gateway) {
|
|
33
|
+
return [GATEWAY_CGI_PATH, "ui/sso/open.cgi", ...(gatewayExternalAccess(gateway) ? [GATEWAY_SETTINGS_CGI_PATH] : [])];
|
|
34
|
+
}
|
|
20
35
|
const record = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
21
36
|
const only = (v, keys) => Object.keys(v).every((k) => keys.includes(k));
|
|
22
37
|
const port = (v) => Number.isInteger(v) && v > 0 && v < 65536;
|
|
@@ -125,6 +140,7 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
|
|
|
125
140
|
if (!record(value) ||
|
|
126
141
|
!only(value, [
|
|
127
142
|
"sdk",
|
|
143
|
+
"external_access",
|
|
128
144
|
"mode",
|
|
129
145
|
"access",
|
|
130
146
|
"upstream_access",
|
|
@@ -145,9 +161,20 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
|
|
|
145
161
|
"token",
|
|
146
162
|
"account",
|
|
147
163
|
])) {
|
|
148
|
-
return invalid("accepts only sdk, mode, access, upstream_access, groups, upstream, listen, passthrough, optional_passthrough, redirects, email_domain, session_ttl, roles, headers, oidc, token and account.");
|
|
164
|
+
return invalid("accepts only sdk, external_access, mode, access, upstream_access, groups, upstream, listen, passthrough, optional_passthrough, redirects, email_domain, session_ttl, roles, headers, oidc, token and account.");
|
|
149
165
|
}
|
|
150
166
|
const v = value;
|
|
167
|
+
let externalAccess;
|
|
168
|
+
if (v.external_access !== undefined) {
|
|
169
|
+
const external = v.external_access;
|
|
170
|
+
if (!record(external) || !only(external, ["connect", "origin_change"]) ||
|
|
171
|
+
(external.connect !== undefined && external.connect !== "optional" && external.connect !== "none") ||
|
|
172
|
+
(external.origin_change !== undefined && external.origin_change !== "none" && external.origin_change !== "confirm")) {
|
|
173
|
+
fail({ code: "STANDALONE_IDENTITY_GATEWAY_EXTERNAL_INVALID", message: "identity_gateway.external_access accepts only connect (optional or none) and origin_change (none or confirm); required is not supported." });
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
externalAccess = { connect: external.connect ?? "optional", origin_change: external.origin_change ?? "none" };
|
|
177
|
+
}
|
|
151
178
|
if (v.launch_path !== undefined) {
|
|
152
179
|
try {
|
|
153
180
|
const raw = v.launch_path;
|
|
@@ -187,6 +214,12 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
|
|
|
187
214
|
return invalid("response_header_timeout_seconds must be 1–3600.");
|
|
188
215
|
if (typeof v.sdk !== "string" || !/^\d+\.\d+\.\d+$/.test(v.sdk))
|
|
189
216
|
return invalid("sdk must be the SDK release (x.y.z) to build the gateway from.");
|
|
217
|
+
if (externalAccess && !externalAccessSdkSupported(v.sdk)) {
|
|
218
|
+
fail({ code: "STANDALONE_IDENTITY_GATEWAY_EXTERNAL_SDK_REQUIRED", message: `identity_gateway.external_access requires SDK ${EXTERNAL_ACCESS_MIN_SDK} or later; SDK 0.28.0 provides the diagnostic ping only.` });
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
if (!externalAccess && externalAccessSdkSupported(v.sdk))
|
|
222
|
+
externalAccess = { connect: "optional", origin_change: "none" };
|
|
190
223
|
if (v.mode !== "none" &&
|
|
191
224
|
v.mode !== "oidc" &&
|
|
192
225
|
v.mode !== "header" &&
|
|
@@ -262,6 +295,7 @@ export function readIdentityGateway(value, daemon, fail, packagedPayload = false
|
|
|
262
295
|
return invalid("header mode needs headers.user.");
|
|
263
296
|
const gateway = {
|
|
264
297
|
sdk: v.sdk,
|
|
298
|
+
...(externalAccess ? { external_access: externalAccess } : {}),
|
|
265
299
|
mode: v.mode,
|
|
266
300
|
...(v.launch_path !== undefined ? { launch_path: v.launch_path } : {}),
|
|
267
301
|
access,
|
|
@@ -405,6 +439,7 @@ export function identityGatewayConfig(m) {
|
|
|
405
439
|
const g = m.identityGateway;
|
|
406
440
|
return (JSON.stringify({
|
|
407
441
|
package: m.package,
|
|
442
|
+
...(gatewayExternalAccess(g) ? { external: gatewayExternalAccess(g) } : {}),
|
|
408
443
|
...(nativeGatewaySupported(m) ? { native_ingress: true } : {}),
|
|
409
444
|
...(runtimeBudgetSdkSupported(g.sdk) ? {
|
|
410
445
|
operation_seconds: operationBudgetSeconds(m),
|
|
@@ -490,6 +525,13 @@ export async function buildIdentityGateway(m, arch, work, run) {
|
|
|
490
525
|
throw new StandaloneBuildError("SDK_DOWNLOAD_FAILED", error.message.replace(/^\[[A-Z_]+\] /, ""));
|
|
491
526
|
}
|
|
492
527
|
runtimeBudgetSource(sdk.goDir, m);
|
|
528
|
+
if (gatewayExternalAccess(m.identityGateway)) {
|
|
529
|
+
const config = join(sdk.goDir, "idgateway/config.go");
|
|
530
|
+
const main = join(sdk.goDir, "idgateway/cmd/gateway/main.go");
|
|
531
|
+
if (!existsSync(config) || !/`json:"external(?:,omitempty)?"`/.test(readFileSync(config, "utf8")) ||
|
|
532
|
+
!existsSync(main) || !/["']\/?settings\.cgi["']/.test(readFileSync(main, "utf8")))
|
|
533
|
+
throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_EXTERNAL_SDK_REQUIRED", `The selected SDK source lacks external access or settings.cgi support; use SDK ${EXTERNAL_ACCESS_MIN_SDK} or later with matching sources.`);
|
|
534
|
+
}
|
|
493
535
|
if (m.identityGateway.oidc?.browser_redirect) {
|
|
494
536
|
const capability = join(sdk.goDir, "idgateway/browser_redirect.go");
|
|
495
537
|
if (!existsSync(capability) || !/const BrowserRedirectVersion = 2\b/.test(readFileSync(capability, "utf8")))
|
|
@@ -36,7 +36,7 @@ import { renderDelegateScopeSteps, } from "../build-spk/delegate-scope.js";
|
|
|
36
36
|
import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
|
|
37
37
|
import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
38
38
|
import { renderUiConfig } from "./dsm-window.js";
|
|
39
|
-
import { identityGatewayConfig, gatewayAdmissionReport, gatewayAdmissionPolicy, readIdentityGateway, } from "./identity-gateway.js";
|
|
39
|
+
import { identityGatewayConfig, gatewayCgiPaths, GATEWAY_SETTINGS_CGI_PATH, gatewayAdmissionReport, gatewayAdmissionPolicy, readIdentityGateway, } from "./identity-gateway.js";
|
|
40
40
|
import { checkCgiBinary } from "./binary.js";
|
|
41
41
|
import { matchesStandaloneScript, renderStandaloneScript, matchesStandaloneScriptPrefix } from "./lifecycle.js";
|
|
42
42
|
import { listPortExposures, loadCompose } from "../compose/analyze.js";
|
|
@@ -133,6 +133,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
133
133
|
if (metadata.daemon_restart !== undefined && (metadata.compose !== undefined || !paths.has("daemon/control")))
|
|
134
134
|
findings.push("DAEMON_RESTART_INVALID daemon restart requires a native daemon controller, without Compose supervision");
|
|
135
135
|
const identityGateway = await inspectProviderOnlyAdmission(dir, paths, metadata.provider_only_oidc, findings);
|
|
136
|
+
await inspectExternalGateway(dir, paths, payload, metadata.gateway_external, findings);
|
|
136
137
|
const installSettings = await inspectInstallSettings(dir, payload, metadata, findings);
|
|
137
138
|
if (metadata.compose !== undefined) {
|
|
138
139
|
const result = await inspectUnifiedCompose(dir, payload, { ...metadata, installSettings }, findings);
|
|
@@ -801,8 +802,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
801
802
|
const binaries = [
|
|
802
803
|
"daemon/control",
|
|
803
804
|
"daemon/payload/_sso/gateway",
|
|
804
|
-
|
|
805
|
-
"ui/sso/open.cgi",
|
|
805
|
+
...(g ? gatewayCgiPaths(g) : []),
|
|
806
806
|
];
|
|
807
807
|
for (const path of g ? binaries : []) {
|
|
808
808
|
const problem = checkCgiBinary(root, join(root, path), arch);
|
|
@@ -1120,6 +1120,39 @@ async function inspectProviderOnlyAdmission(dir, paths, saved, findings) {
|
|
|
1120
1120
|
return;
|
|
1121
1121
|
}
|
|
1122
1122
|
}
|
|
1123
|
+
/** External settings are the same package-account gateway binary as the existing CGI entries. */
|
|
1124
|
+
async function inspectExternalGateway(dir, paths, payload, saved, findings) {
|
|
1125
|
+
if (!paths.has("sso/gateway.json") && saved === undefined && !paths.has(GATEWAY_SETTINGS_CGI_PATH))
|
|
1126
|
+
return;
|
|
1127
|
+
try {
|
|
1128
|
+
const info = readInfo(join(dir, "INFO"), [], "binary");
|
|
1129
|
+
const root = join(dir, "external-gateway-inspection");
|
|
1130
|
+
mkdirSync(root);
|
|
1131
|
+
const entries = ["sso/gateway.json", GATEWAY_SETTINGS_CGI_PATH, "ui/sso/open.cgi"];
|
|
1132
|
+
await tar.x({ file: join(dir, "package.tgz"), cwd: root, filter: p => entries.includes(p.replace(/^\.\//, "")) });
|
|
1133
|
+
const cfg = JSON.parse(readFileSync(join(root, "sso/gateway.json"), "utf8"));
|
|
1134
|
+
if (saved === undefined && cfg.external === undefined && !paths.has(GATEWAY_SETTINGS_CGI_PATH))
|
|
1135
|
+
return;
|
|
1136
|
+
const external = saved;
|
|
1137
|
+
if (!external || typeof external !== "object" || Array.isArray(external) ||
|
|
1138
|
+
Object.keys(external).length !== 2 || (external.connect !== "optional" && external.connect !== "none") ||
|
|
1139
|
+
(external.origin_change !== "none" && external.origin_change !== "confirm") ||
|
|
1140
|
+
!isDeepStrictEqual(cfg.external, external) || cfg.package !== info.package)
|
|
1141
|
+
throw new Error("external configuration differs from its package declaration");
|
|
1142
|
+
const arch = info.arch.split(/\s+/).map(familyOfArchToken).find(Boolean) ?? "x86_64";
|
|
1143
|
+
const path = join(root, GATEWAY_SETTINGS_CGI_PATH);
|
|
1144
|
+
const problem = checkCgiBinary(root, path, arch);
|
|
1145
|
+
if (problem)
|
|
1146
|
+
throw new Error(problem.message);
|
|
1147
|
+
if (payload.find(p => p.path === GATEWAY_SETTINGS_CGI_PATH)?.mode !== "0755")
|
|
1148
|
+
throw new Error("settings.cgi must be executable");
|
|
1149
|
+
if (!readFileSync(path).equals(readFileSync(join(root, "ui/sso/open.cgi"))))
|
|
1150
|
+
throw new Error("settings.cgi must be the packaged gateway executable");
|
|
1151
|
+
}
|
|
1152
|
+
catch (e) {
|
|
1153
|
+
findings.push(`GATEWAY_EXTERNAL_INVALID ${e.message}`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1123
1156
|
async function recoverNativeGateway(dir, value, info, findings) {
|
|
1124
1157
|
if (value === undefined)
|
|
1125
1158
|
return undefined;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Only the fixed DSM CGI receives a resume token; external origins are resolved by the SDK. */
|
|
2
2
|
export declare function oidcResumeRuntime(pkg: string, ports: number[]): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Only the fixed DSM CGI receives a resume token; external origins are resolved by the SDK. */
|
|
2
2
|
export function oidcResumeRuntime(pkg, ports) {
|
|
3
3
|
return `(function(param) {
|
|
4
4
|
if (typeof param === 'string') {
|
|
@@ -14,13 +14,18 @@ export function oidcResumeRuntime(pkg, ports) {
|
|
|
14
14
|
}
|
|
15
15
|
if (!param || typeof param !== 'object' || Array.isArray(param)) return false;
|
|
16
16
|
var value = param && param.ssoResume;
|
|
17
|
-
if (typeof value !== 'string') return false;
|
|
17
|
+
if (typeof value !== 'string' || /\\s/.test(value)) return false;
|
|
18
|
+
var external = /^[A-Za-z0-9_-]{1,128}:x[01]:[0-9]{1,19}\\.[0-9]{1,19}$/.test(value);
|
|
18
19
|
var parts = /^([A-Za-z0-9_-]{1,128}):([0-9]{1,5})$/.exec(value);
|
|
19
|
-
if (!parts || ${JSON.stringify(ports)}.indexOf(Number(parts[2])) < 0) return false;
|
|
20
|
+
if (!external && (!parts || ${JSON.stringify(ports)}.indexOf(Number(parts[2])) < 0)) return false;
|
|
20
21
|
var target = new URL(${JSON.stringify(`/webman/3rdparty/${pkg}/sso/authorize.cgi`)},location.origin);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (external) {
|
|
23
|
+
target.searchParams.set('resume',value);
|
|
24
|
+
} else {
|
|
25
|
+
var app = new URL(location.origin); app.port = parts[2]; app.pathname = '/_sso/dsm-return';
|
|
26
|
+
target.searchParams.set('return',app.href);
|
|
27
|
+
target.searchParams.set('txn',parts[1]);
|
|
28
|
+
}
|
|
24
29
|
target.searchParams.set('login_attempt','1');
|
|
25
30
|
location.replace(target.href);
|
|
26
31
|
return true;
|
package/dist/standalone/spk.js
CHANGED
|
@@ -31,7 +31,7 @@ import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
|
31
31
|
import { writeComposePayload } from "./compose.js";
|
|
32
32
|
import { copyDaemonPayload, daemonSdkVersion, nodeDependency } from "./daemon.js";
|
|
33
33
|
import { DAEMON_INGRESS_CONFIG, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
34
|
-
import {
|
|
34
|
+
import { gatewayCgiPaths, gatewayExternalAccess, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig, identityGatewayDeclaration, } from "./identity-gateway.js";
|
|
35
35
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS, } from "./background.js";
|
|
36
36
|
import { renderStandaloneResource } from "./resource.js";
|
|
37
37
|
import { nativePortal } from "./portal.js";
|
|
@@ -112,8 +112,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
112
112
|
if (!options.identityGateway)
|
|
113
113
|
throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_ARTIFACT_REQUIRED", "identity gateway artifact required.");
|
|
114
114
|
for (const path of [
|
|
115
|
-
|
|
116
|
-
"ui/sso/open.cgi",
|
|
115
|
+
...gatewayCgiPaths(manifest.identityGateway),
|
|
117
116
|
`daemon/payload/${GATEWAY_EXEC}`,
|
|
118
117
|
]) {
|
|
119
118
|
const target = join(staging, "package", path);
|
|
@@ -232,6 +231,8 @@ export async function assembleStandaloneSpk(options) {
|
|
|
232
231
|
...(options.apiDescription ? { ctl: { hash: options.apiDescription.sha256, registry: options.apiRegistry } } : {}),
|
|
233
232
|
backend: manifest.backend.kind,
|
|
234
233
|
...(gatewayAdmissionPolicy(manifest.identityGateway, manifest.entry) ? { provider_only_oidc: gatewayAdmissionPolicy(manifest.identityGateway, manifest.entry) } : {}),
|
|
234
|
+
...(manifest.identityGateway && gatewayExternalAccess(manifest.identityGateway)
|
|
235
|
+
? { gateway_external: gatewayExternalAccess(manifest.identityGateway) } : {}),
|
|
235
236
|
...(nativeGatewaySupported(manifest) ? { native_gateway: { sdk: manifest.identityGateway.sdk, listen: manifest.identityGateway.listen } } : {}),
|
|
236
237
|
...(manifest.daemon?.ingress ? { daemon_ingress: manifest.daemon.ingress } : {}),
|
|
237
238
|
...(manifest.daemon?.node ? { daemon_node: manifest.daemon.node } : {}),
|
|
@@ -410,7 +411,7 @@ function copyUiTree(source, target) {
|
|
|
410
411
|
throw new StandaloneBuildError("STANDALONE_UI_TOO_LARGE", "The UI directory exceeds 64 MiB.");
|
|
411
412
|
}
|
|
412
413
|
copyFileSync(src, join(to, name));
|
|
413
|
-
chmodSync(join(to, name), FILE_MODE);
|
|
414
|
+
chmodSync(join(to, name), name.endsWith(".cgi") ? EXEC_MODE : FILE_MODE);
|
|
414
415
|
}
|
|
415
416
|
else {
|
|
416
417
|
throw new StandaloneBuildError("STANDALONE_UI_SPECIAL_FILE", `The UI directory may contain only regular files and directories: ${src}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
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.29.0",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|
|
@@ -35,16 +35,31 @@ and `identity_gateway` for headless projects. Optional `compose.helpers` names
|
|
|
35
35
|
architecture-specific native payloads for token/account adapters. All lifecycle
|
|
36
36
|
scripts run as the package account; containers are managed by Container Manager.
|
|
37
37
|
|
|
38
|
+
For external access, pin both `compose.sdk` and `identity_gateway.sdk` to
|
|
39
|
+
0.29.0 or later and use the gateway's
|
|
40
|
+
[`external_access` policy and DSM settings entry](identity-gateway.md#external-access).
|
|
41
|
+
There are no installation-wizard URL fields or post-install state initialization.
|
|
42
|
+
Manual URL configuration does not require Connect. The SDK resolves the public
|
|
43
|
+
origin for Compose; restart the App after changing an origin used by its
|
|
44
|
+
environment or a generated private snapshot.
|
|
45
|
+
|
|
38
46
|
## Startup budget
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
Omit `compose.startup_timeout_seconds`: the default of 900 seconds lets a loaded
|
|
49
|
+
HDD NAS with 2 GB RAM start the App. Declare a shorter value only with device
|
|
50
|
+
evidence; only integers from 2 through 900 are accepted.
|
|
42
51
|
Lock waiting, status, readiness, stability and handoff share that deadline; there
|
|
43
52
|
is no extra fixed handoff allowance. The cap is a policy bound, not a measured
|
|
44
53
|
DSM lifecycle limit. `daemon: {timeouts: {stop: 120, status: 15, uninstall: 300}}`
|
|
45
54
|
configures the other lifecycle phases without declaring a daemon payload.
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
One readiness probe may take 30 seconds. A health failure after startup closes
|
|
56
|
+
new ingress and the gateway shows a localized, self-refreshing starting page
|
|
57
|
+
(`app_starting` for API clients) while DSM status stays running; the App gets
|
|
58
|
+
the startup budget to recover before its gateway stops. Authority or generation
|
|
59
|
+
failures stop at once. The SDK raises stricter Compose healthcheck timing to
|
|
60
|
+
timeout 30s, retries 3 and start_period equal to the startup budget; declare the
|
|
61
|
+
health command only, without timing or short in-command limits such as
|
|
62
|
+
`curl --max-time 2`.
|
|
48
63
|
Use a matching SDK that implements the field and rebuild the App; older planners
|
|
49
64
|
are rejected for explicit budgets. The SDK plan drives generated daemon start
|
|
50
65
|
and supplies every container with `SYNOPKGLAND_STARTUP_BUDGET_SECONDS`, overriding
|
|
@@ -93,7 +108,7 @@ a build operator trust decision. `check`/`inspect` remain offline.
|
|
|
93
108
|
|
|
94
109
|
| Feature | Current behavior |
|
|
95
110
|
| --- | --- |
|
|
96
|
-
| Files in one directory, profiles, anchors, depends_on, healthcheck | Reference-loader semantics retained; DSM worker compatibility needs device acceptance |
|
|
111
|
+
| Files in one directory, profiles, anchors, depends_on, healthcheck | Reference-loader semantics retained; healthcheck timing receives the SDK slow-NAS floor; DSM worker compatibility needs device acceptance |
|
|
97
112
|
| Named local volumes | Stable package namespace, retained across recreation; inspect inventory |
|
|
98
113
|
| Private bind data | DSM build rejects COMPOSE_PRIVATE_BIND_TIMING_UNSUPPORTED: clean-install preinst precedes pkgvar creation, while worker acquire precedes postinst; no root initialization or automatic volume conversion |
|
|
99
114
|
| Image VOLUME | Build inventories verified image config and assigns a stable named volume |
|
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Wrap an OSS app with DSM accounts
|
|
2
2
|
|
|
3
|
+
## External access
|
|
4
|
+
|
|
5
|
+
Use SDK 0.29.0 or later with matching gateway sources. Add this to the existing
|
|
6
|
+
complete gateway declaration; keep the App's upstream, listener and auth policy:
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
identity_gateway:
|
|
10
|
+
sdk: 0.29.0
|
|
11
|
+
external_access:
|
|
12
|
+
connect: optional # optional | none
|
|
13
|
+
origin_change: none # none | confirm
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Omitting the block or either member uses `connect: optional` and
|
|
17
|
+
`origin_change: none`. `connect: required`, unknown members and other values
|
|
18
|
+
are rejected. SDKs below 0.29.0 reject an explicit block, including an empty
|
|
19
|
+
one. SDK 0.28.0 supplies the diagnostic ping without external access. On
|
|
20
|
+
Compose Apps, `compose.sdk` and `identity_gateway.sdk` must name the same
|
|
21
|
+
release. Build also rejects a development SDK override without the external
|
|
22
|
+
configuration and settings CGI dispatch support.
|
|
23
|
+
|
|
24
|
+
The generated `target/sso/gateway.json` uses
|
|
25
|
+
`"external":{"connect":"optional","origin_change":"none"}`. Older SDK targets
|
|
26
|
+
omit this member and the settings entry. The SPK packages the gateway
|
|
27
|
+
executable as `ui/sso/settings.cgi` with mode 0755, alongside `open.cgi` and
|
|
28
|
+
`authorize.cgi`; it runs as the package account. `inspect` checks the policy
|
|
29
|
+
inventory, executable mode and matching gateway bytes.
|
|
30
|
+
|
|
31
|
+
An administrator opens
|
|
32
|
+
`https://<NAS>:<DSM port>/webman/3rdparty/<package>/sso/settings.cgi` to view the
|
|
33
|
+
LAN, manual, Connect and private URLs. The initial GET only bootstraps the DSM
|
|
34
|
+
token; protected data and writes require a token-bearing POST, a valid DSM
|
|
35
|
+
session and administrator access. Manual configuration requires no Connect
|
|
36
|
+
installation. It edits only `manual_origin` and `dsm_origin` in private
|
|
37
|
+
`<StateDir>/external.json`; do not package initial state or installation-wizard
|
|
38
|
+
URL fields.
|
|
39
|
+
|
|
40
|
+
Public origins must use HTTPS and a DNS hostname. Port 443 is normalized;
|
|
41
|
+
other ports, IP addresses, paths, queries and user information are rejected,
|
|
42
|
+
as is an origin equal to the DSM origin. Manual takes precedence, then the
|
|
43
|
+
Connect projection when `connect: optional`; the first effective origin is
|
|
44
|
+
primary. `connect: none` ignores that projection. Private URLs are display-only.
|
|
45
|
+
Use `origin_change: confirm` for an App that must confirm public-origin changes.
|
|
46
|
+
|
|
47
|
+
Cold DSM login preserves external resume tokens
|
|
48
|
+
`<txn>:x<n>:<r1>.<r2>` in the fixed `authorize.cgi?resume=<encoded token>`
|
|
49
|
+
handoff. The transaction contains 1–128 ASCII letters, digits, underscores or
|
|
50
|
+
hyphens; `n` is 0 or 1 and each revision is a 1–19 digit decimal string. The
|
|
51
|
+
renderer never converts revisions through JavaScript Number or constructs an
|
|
52
|
+
external URL. The SDK checks origin, browser binding and current revisions;
|
|
53
|
+
stale revisions and unavailable indices fall back to LAN login. Local
|
|
54
|
+
`<txn>:<declared port>` resume remains supported. The SDK's `open.cgi` owns
|
|
55
|
+
resume handling for classless URL launchers, including Compose and `ui: false`.
|
|
56
|
+
|
|
57
|
+
Connect forwards to an administrator-approved App name and port. The ping is
|
|
58
|
+
a routing diagnostic; other services on that same listener may also be
|
|
59
|
+
reachable. It does not prove that all content belongs to the App. Existing
|
|
60
|
+
App authentication and passthrough policy still apply. Public provider limits
|
|
61
|
+
also apply: Cloudflare edge enforces the zone's upload limit, and long
|
|
62
|
+
responses need heartbeats within 60 seconds; use SSE or
|
|
63
|
+
`Cache-Control: no-transform` for streaming.
|
|
64
|
+
|
|
3
65
|
## Per-request OIDC browser origins (development)
|
|
4
66
|
|
|
5
67
|
`oidc.browser_redirect: {}` opts into per-request browser origins for SDK/CLI
|
|
@@ -115,7 +115,7 @@ Use existing `daemon.timeouts` for start/stop/status/uninstall (defaults
|
|
|
115
115
|
30/30/5/60 seconds). Start/stop/uninstall accept 2–3600 seconds and status accepts
|
|
116
116
|
1–3600. A `daemon` containing only `timeouts` declares budgets without an
|
|
117
117
|
executable, including background and Compose Apps; Compose start still uses
|
|
118
|
-
`compose.startup_timeout_seconds` (
|
|
118
|
+
`compose.startup_timeout_seconds` (900, 2–900). Background user-manager recovery
|
|
119
119
|
uses the start allowance and its generated package user unit uses the stop
|
|
120
120
|
allowance with `KillMode=control-group`.
|
|
121
121
|
|