@synopackageland/cli 0.12.1 → 0.13.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 +17 -0
- package/assets/go/daemon/main.go +25 -0
- package/assets/go/daemon/network.go +107 -0
- package/assets/go/daemon/network_test.go +86 -0
- package/dist/error-registry.js +15 -0
- package/dist/runtime/executor-digests.json +1 -12
- package/dist/skill.js +1 -0
- package/dist/standalone/build.js +5 -1
- package/dist/standalone/compose.d.ts +22 -0
- package/dist/standalone/compose.js +90 -0
- package/dist/standalone/daemon.d.ts +2 -0
- package/dist/standalone/daemon.js +22 -14
- package/dist/standalone/dsm-window.d.ts +3 -0
- package/dist/standalone/dsm-window.js +30 -1
- package/dist/standalone/identity-gateway.d.ts +73 -0
- package/dist/standalone/identity-gateway.js +215 -0
- package/dist/standalone/inspect-spk.js +40 -10
- package/dist/standalone/launch-params.d.ts +11 -0
- package/dist/standalone/launch-params.js +33 -0
- package/dist/standalone/lifecycle.js +6 -0
- package/dist/standalone/manifest.d.ts +5 -0
- package/dist/standalone/manifest.js +12 -2
- package/dist/standalone/reopen-events.d.ts +2 -2
- package/dist/standalone/reopen-events.js +3 -3
- package/dist/standalone/resource.d.ts +2 -0
- package/dist/standalone/resource.js +2 -0
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +20 -3
- package/dist/validate.js +1 -1
- package/package.json +2 -2
- package/skills/SKILL.md +1 -1
- package/skills/reference/create.md +2 -2
- package/skills/reference/identity-gateway.md +174 -0
- package/skills/reference/wrap.md +4 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
import type { CommandRunner } from "./build.js";
|
|
3
|
+
import { type Daemon } from "./daemon.js";
|
|
4
|
+
import type { StandaloneError, StandaloneManifest } from "./manifest.js";
|
|
5
|
+
/** The SDK's identity gateway: the daemon wrapper and the DSM-origin authorize CGI. */
|
|
6
|
+
export declare const GATEWAY_MAIN = "./idgateway/cmd/gateway";
|
|
7
|
+
/** Served by DSM at /webman/3rdparty/<package>/sso/authorize.cgi. */
|
|
8
|
+
export declare const GATEWAY_CGI_PATH = "ui/sso/authorize.cgi";
|
|
9
|
+
/** The gateway inside the daemon payload; the App's payload must leave _sso/ free. */
|
|
10
|
+
export declare const GATEWAY_PAYLOAD_DIR = "_sso";
|
|
11
|
+
export declare const GATEWAY_EXEC = "_sso/gateway";
|
|
12
|
+
export declare const GATEWAY_CONFIG_PATH = "sso/gateway.json";
|
|
13
|
+
export type GatewayMode = "oidc" | "header" | "token";
|
|
14
|
+
export interface GatewayRoles {
|
|
15
|
+
claim: string;
|
|
16
|
+
admin: string;
|
|
17
|
+
user?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GatewayClient {
|
|
20
|
+
id: string;
|
|
21
|
+
redirect_paths: string[];
|
|
22
|
+
native_redirects: string[];
|
|
23
|
+
require_pkce: boolean;
|
|
24
|
+
roles?: GatewayRoles;
|
|
25
|
+
}
|
|
26
|
+
/** `identity_gateway`: signs DSM users in to the App's own web UI. */
|
|
27
|
+
export interface IdentityGateway {
|
|
28
|
+
/** The SDK release the gateway is built from. */
|
|
29
|
+
sdk: string;
|
|
30
|
+
mode: GatewayMode;
|
|
31
|
+
/** The App's loopback port. */
|
|
32
|
+
upstream: number;
|
|
33
|
+
listen: {
|
|
34
|
+
http?: number;
|
|
35
|
+
https?: number;
|
|
36
|
+
};
|
|
37
|
+
passthrough: string[];
|
|
38
|
+
redirects: Record<string, string>;
|
|
39
|
+
email_domain?: string;
|
|
40
|
+
session_ttl?: number;
|
|
41
|
+
roles?: GatewayRoles;
|
|
42
|
+
headers?: Partial<Record<"user" | "email" | "name" | "groups" | "uid", string>>;
|
|
43
|
+
oidc?: {
|
|
44
|
+
issuer_port: number;
|
|
45
|
+
clients: GatewayClient[];
|
|
46
|
+
rewrite_json: {
|
|
47
|
+
method: string;
|
|
48
|
+
path: string;
|
|
49
|
+
field: string;
|
|
50
|
+
}[];
|
|
51
|
+
};
|
|
52
|
+
token?: {
|
|
53
|
+
exec: string;
|
|
54
|
+
args: string[];
|
|
55
|
+
};
|
|
56
|
+
/** Account deletion: the gateway runs on_delete when a DSM user that signed in is deleted. */
|
|
57
|
+
account?: {
|
|
58
|
+
on_delete: DeleteAction[];
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** One delete step; {uid} and {name} are the only placeholders. */
|
|
62
|
+
export type DeleteAction = {
|
|
63
|
+
remove: string;
|
|
64
|
+
} | {
|
|
65
|
+
exec: string[];
|
|
66
|
+
};
|
|
67
|
+
export declare function readIdentityGateway(value: unknown, daemon: Pick<Daemon, "targets"> | undefined, fail: (error: StandaloneError) => void): IdentityGateway | undefined;
|
|
68
|
+
/** target/sso/gateway.json, the SDK gateway's configuration (idgateway.Config). */
|
|
69
|
+
export declare function identityGatewayConfig(m: StandaloneManifest): string;
|
|
70
|
+
/** The daemon as the controller runs it: the gateway, with the App as its child. */
|
|
71
|
+
export declare function gatewayDaemon(daemon: Daemon): Daemon;
|
|
72
|
+
/** Builds the gateway from the declared SDK release with the pinned Go, binding its config path. */
|
|
73
|
+
export declare function buildIdentityGateway(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<string | undefined>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from "../build-spk/go-toolchain.js";
|
|
4
|
+
import { goChildEnv } from "../runtime/go-env.js";
|
|
5
|
+
import { sdkSource } from "../runtime/sdk-source.js";
|
|
6
|
+
import { checkCgiBinary } from "./binary.js";
|
|
7
|
+
import { relativeExec, token } from "./daemon.js";
|
|
8
|
+
import { StandaloneBuildError } from "./spk.js";
|
|
9
|
+
/** The SDK's identity gateway: the daemon wrapper and the DSM-origin authorize CGI. */
|
|
10
|
+
export const GATEWAY_MAIN = "./idgateway/cmd/gateway";
|
|
11
|
+
/** Served by DSM at /webman/3rdparty/<package>/sso/authorize.cgi. */
|
|
12
|
+
export const GATEWAY_CGI_PATH = "ui/sso/authorize.cgi";
|
|
13
|
+
/** The gateway inside the daemon payload; the App's payload must leave _sso/ free. */
|
|
14
|
+
export const GATEWAY_PAYLOAD_DIR = "_sso";
|
|
15
|
+
export const GATEWAY_EXEC = `${GATEWAY_PAYLOAD_DIR}/gateway`;
|
|
16
|
+
export const GATEWAY_CONFIG_PATH = "sso/gateway.json";
|
|
17
|
+
const record = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
18
|
+
const only = (v, keys) => Object.keys(v).every(k => keys.includes(k));
|
|
19
|
+
const port = (v) => Number.isInteger(v) && v > 0 && v < 65536;
|
|
20
|
+
/** An absolute local path without dot segments, backslashes or a query. */
|
|
21
|
+
const localPath = (v) => typeof v === "string" && v.length <= 512 && /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/.test(v)
|
|
22
|
+
&& !v.startsWith("//") && !v.split("/").some(p => p === "." || p === "..");
|
|
23
|
+
const strings = (v, ok, max = 64) => Array.isArray(v) && v.length <= max && v.every(ok);
|
|
24
|
+
const name = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(v);
|
|
25
|
+
const headerName = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(v);
|
|
26
|
+
const claimValue = (v) => typeof v === "string" && /^[A-Za-z0-9_.:-]{1,64}$/.test(v);
|
|
27
|
+
/** A mobile app's custom-scheme redirect; web schemes are same-origin redirect_paths instead. */
|
|
28
|
+
const nativeRedirect = (v) => typeof v === "string" && v.length <= 512 && /^[a-z][a-z0-9+.-]*:[^\s]*$/.test(v)
|
|
29
|
+
&& !/^(https?|javascript|data|vbscript|file|blob|about):/.test(v);
|
|
30
|
+
const DSM_PORTS = [5000, 5001];
|
|
31
|
+
/** Only {uid} and {name}, which the gateway expands inside one argv element or path element. */
|
|
32
|
+
const placeholders = (v) => (v.match(/\{[^{}]*\}/g) ?? []).every(p => p === "{uid}" || p === "{name}");
|
|
33
|
+
/** A clean path under the package var directory that names the account, outside the gateway's sso/. */
|
|
34
|
+
const removePath = (v) => typeof v === "string" && v.length <= 512 && placeholders(v)
|
|
35
|
+
&& /^[A-Za-z0-9_{}][A-Za-z0-9_.{}-]*(\/[A-Za-z0-9_.{}-]+)*$/.test(v) && !v.split("/").some(p => p === "." || p === "..")
|
|
36
|
+
&& v.split("/")[0] !== "sso" && /\{(uid|name)\}/.test(v);
|
|
37
|
+
function readAccount(v, daemon) {
|
|
38
|
+
if (!record(v) || !only(v, ["on_delete"]) || !Array.isArray(v.on_delete) || v.on_delete.length > 16)
|
|
39
|
+
return "account accepts on_delete, a list of at most 16 actions.";
|
|
40
|
+
const actions = [];
|
|
41
|
+
for (const a of v.on_delete) {
|
|
42
|
+
if (record(a) && only(a, ["remove"]) && removePath(a.remove)) {
|
|
43
|
+
actions.push({ remove: a.remove });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!record(a) || !only(a, ["exec"]) || !Array.isArray(a.exec) || !relativeExec(a.exec[0]) || !strings(a.exec.slice(1), s => token(s) && placeholders(s))) {
|
|
47
|
+
return "each account.on_delete action is { remove: <path under the var directory naming {uid} or {name}> } or { exec: [<program in the daemon payload>, args...] }; {uid} and {name} are the only placeholders.";
|
|
48
|
+
}
|
|
49
|
+
const argv = a.exec;
|
|
50
|
+
if (!Object.values(daemon.targets).every(dir => existsSync(join(dir, argv[0]))))
|
|
51
|
+
return `account.on_delete exec ${argv[0]} is not in the daemon payload.`;
|
|
52
|
+
actions.push({ exec: argv });
|
|
53
|
+
}
|
|
54
|
+
return { on_delete: actions };
|
|
55
|
+
}
|
|
56
|
+
function readRoles(v) {
|
|
57
|
+
if (v === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (!record(v) || !only(v, ["claim", "admin", "user"]))
|
|
60
|
+
return false;
|
|
61
|
+
const roles = { claim: v.claim ?? "groups", admin: v.admin ?? "admin", ...(v.user !== undefined ? { user: v.user } : {}) };
|
|
62
|
+
return claimValue(roles.claim) && claimValue(roles.admin) && (roles.user === undefined || claimValue(roles.user)) ? roles : false;
|
|
63
|
+
}
|
|
64
|
+
function readClient(v) {
|
|
65
|
+
if (!record(v) || !only(v, ["id", "redirect_paths", "native_redirects", "pkce", "roles"]))
|
|
66
|
+
return undefined;
|
|
67
|
+
const native = v.native_redirects ?? [], roles = readRoles(v.roles);
|
|
68
|
+
if (!name(v.id) || !strings(v.redirect_paths, localPath) || !strings(native, nativeRedirect) || (v.pkce !== undefined && typeof v.pkce !== "boolean") || roles === false)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (!v.redirect_paths.length && !native.length)
|
|
71
|
+
return undefined;
|
|
72
|
+
return { id: v.id, redirect_paths: v.redirect_paths, native_redirects: native, require_pkce: v.pkce === true, ...(roles ? { roles } : {}) };
|
|
73
|
+
}
|
|
74
|
+
export function readIdentityGateway(value, daemon, fail) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const invalid = (why) => { fail({ code: "STANDALONE_IDENTITY_GATEWAY_INVALID", message: `identity_gateway: ${why}` }); return undefined; };
|
|
78
|
+
if (!daemon)
|
|
79
|
+
return invalid("needs daemon (native child) or compose (Container Manager upstream).");
|
|
80
|
+
if (!record(value) || !only(value, ["sdk", "mode", "upstream", "listen", "passthrough", "redirects", "email_domain", "session_ttl", "roles", "headers", "oidc", "token", "account"])) {
|
|
81
|
+
return invalid("accepts only sdk, mode, upstream, listen, passthrough, redirects, email_domain, session_ttl, roles, headers, oidc, token and account.");
|
|
82
|
+
}
|
|
83
|
+
const v = value;
|
|
84
|
+
if (typeof v.sdk !== "string" || !/^\d+\.\d+\.\d+$/.test(v.sdk))
|
|
85
|
+
return invalid("sdk must be the SDK release (x.y.z) to build the gateway from.");
|
|
86
|
+
if (v.mode !== "oidc" && v.mode !== "header" && v.mode !== "token")
|
|
87
|
+
return invalid("mode must be oidc, header or token.");
|
|
88
|
+
if (!port(v.upstream))
|
|
89
|
+
return invalid("upstream must be the App's loopback port.");
|
|
90
|
+
const listen = v.listen;
|
|
91
|
+
if (!record(listen) || !only(listen, ["http", "https"]) || (listen.http === undefined && listen.https === undefined)
|
|
92
|
+
|| ![listen.http, listen.https].every(p => p === undefined || port(p)))
|
|
93
|
+
return invalid("listen needs an http and/or https port.");
|
|
94
|
+
const issuer = record(v.oidc) ? v.oidc.issuer_port : undefined;
|
|
95
|
+
const ports = [listen.http, listen.https, v.upstream, issuer].filter(p => p !== undefined);
|
|
96
|
+
if (new Set(ports).size !== ports.length || ports.some(p => DSM_PORTS.includes(p)))
|
|
97
|
+
return invalid("listen, upstream and oidc.issuer_port must be distinct and not DSM's 5000/5001.");
|
|
98
|
+
const passthrough = v.passthrough ?? [];
|
|
99
|
+
if (!strings(passthrough, p => localPath(p) && p !== "/" && !/^\/_sso(\/|$)/.test(p)))
|
|
100
|
+
return invalid("passthrough must be path prefixes such as /share/, outside /_sso.");
|
|
101
|
+
const redirects = v.redirects ?? {};
|
|
102
|
+
if (!record(redirects) || Object.entries(redirects).some(([from, to]) => !localPath(from) || !localPath(to)))
|
|
103
|
+
return invalid("redirects maps local paths to local paths.");
|
|
104
|
+
if (v.email_domain !== undefined && (typeof v.email_domain !== "string" || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(v.email_domain)))
|
|
105
|
+
return invalid("email_domain must be a domain name.");
|
|
106
|
+
if (v.session_ttl !== undefined && (!Number.isInteger(v.session_ttl) || v.session_ttl < 60 || v.session_ttl > 86400))
|
|
107
|
+
return invalid("session_ttl must be 60-86400 seconds.");
|
|
108
|
+
const roles = readRoles(v.roles);
|
|
109
|
+
if (roles === false)
|
|
110
|
+
return invalid("roles accepts claim, admin and user values.");
|
|
111
|
+
const headers = v.headers;
|
|
112
|
+
if (headers !== undefined && (!record(headers) || !only(headers, ["user", "email", "name", "groups", "uid"]) || !Object.values(headers).every(headerName))) {
|
|
113
|
+
return invalid("headers maps user, email, name, groups and uid to header names.");
|
|
114
|
+
}
|
|
115
|
+
if (v.mode === "header" && !(record(headers) && headers.user))
|
|
116
|
+
return invalid("header mode needs headers.user.");
|
|
117
|
+
const gateway = {
|
|
118
|
+
sdk: v.sdk, mode: v.mode, upstream: v.upstream, listen: { ...(listen.http ? { http: listen.http } : {}), ...(listen.https ? { https: listen.https } : {}) },
|
|
119
|
+
passthrough: passthrough, redirects: redirects,
|
|
120
|
+
...(v.email_domain ? { email_domain: v.email_domain } : {}), ...(v.session_ttl ? { session_ttl: v.session_ttl } : {}),
|
|
121
|
+
...(roles ? { roles } : {}), ...(headers ? { headers: headers } : {}),
|
|
122
|
+
};
|
|
123
|
+
if (v.mode === "oidc") {
|
|
124
|
+
const o = v.oidc;
|
|
125
|
+
if (!record(o) || !only(o, ["issuer_port", "clients", "rewrite_json"]) || !port(o.issuer_port) || !Array.isArray(o.clients) || !o.clients.length || o.clients.length > 16) {
|
|
126
|
+
return invalid("oidc mode needs oidc.issuer_port and clients.");
|
|
127
|
+
}
|
|
128
|
+
const clients = o.clients.map(readClient);
|
|
129
|
+
if (clients.some(c => !c) || new Set(clients.map(c => c.id)).size !== clients.length)
|
|
130
|
+
return invalid("each oidc client needs a unique id and redirect_paths (local paths) or native_redirects (custom schemes).");
|
|
131
|
+
const rewrite = o.rewrite_json ?? [];
|
|
132
|
+
if (!Array.isArray(rewrite) || !rewrite.every(r => record(r) && only(r, ["method", "path", "field"]) && /^[A-Z]{3,7}$/.test(String(r.method)) && localPath(r.path) && name(r.field))) {
|
|
133
|
+
return invalid("oidc.rewrite_json entries are { method, path, field }.");
|
|
134
|
+
}
|
|
135
|
+
gateway.oidc = { issuer_port: o.issuer_port, clients: clients, rewrite_json: rewrite };
|
|
136
|
+
}
|
|
137
|
+
if (v.mode === "token") {
|
|
138
|
+
const t = v.token;
|
|
139
|
+
const args = record(t) ? t.args ?? [] : [];
|
|
140
|
+
if (!record(t) || !only(t, ["exec", "args"]) || !relativeExec(t.exec) || !strings(args, token))
|
|
141
|
+
return invalid("token mode needs token.exec, a program in the daemon payload.");
|
|
142
|
+
if (!Object.values(daemon.targets).every(dir => existsSync(join(dir, t.exec))))
|
|
143
|
+
return invalid(`token.exec ${t.exec} is not in the daemon payload.`);
|
|
144
|
+
gateway.token = { exec: t.exec, args: args };
|
|
145
|
+
}
|
|
146
|
+
if (v.account !== undefined) {
|
|
147
|
+
const account = readAccount(v.account, daemon);
|
|
148
|
+
if (typeof account === "string")
|
|
149
|
+
return invalid(account);
|
|
150
|
+
gateway.account = account;
|
|
151
|
+
}
|
|
152
|
+
if (Object.values(daemon.targets).some(dir => { try {
|
|
153
|
+
lstatSync(join(dir, GATEWAY_PAYLOAD_DIR));
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
} })) {
|
|
159
|
+
return invalid(`the daemon payload must not contain ${GATEWAY_PAYLOAD_DIR}/, where the gateway is installed.`);
|
|
160
|
+
}
|
|
161
|
+
return gateway;
|
|
162
|
+
}
|
|
163
|
+
const packageDest = (m) => `/var/packages/${m.package}/target`;
|
|
164
|
+
/** target/sso/gateway.json, the SDK gateway's configuration (idgateway.Config). */
|
|
165
|
+
export function identityGatewayConfig(m) {
|
|
166
|
+
const g = m.identityGateway;
|
|
167
|
+
return JSON.stringify({
|
|
168
|
+
package: m.package, mode: g.mode, upstream: `http://127.0.0.1:${g.upstream}`, listen: g.listen,
|
|
169
|
+
authorize_cgi: `/webman/3rdparty/${m.package}/sso/authorize.cgi`, state_dir: `/var/packages/${m.package}/var/sso`,
|
|
170
|
+
passthrough: g.passthrough, redirects: g.redirects,
|
|
171
|
+
...(g.email_domain ? { email_domain: g.email_domain } : {}), ...(g.session_ttl ? { session_ttl_seconds: g.session_ttl } : {}),
|
|
172
|
+
...(g.roles ? { roles: g.roles } : {}), ...(g.headers ? { headers: g.headers } : {}),
|
|
173
|
+
...(g.oidc ? { oidc: g.oidc } : {}),
|
|
174
|
+
...(g.token ? { token: { command: [`${packageDest(m)}/daemon/payload/${g.token.exec}`, ...g.token.args] } } : {}),
|
|
175
|
+
...(g.account ? { account: {
|
|
176
|
+
var_dir: `/var/packages/${m.package}/var`,
|
|
177
|
+
on_delete: g.account.on_delete.map(a => "remove" in a ? a : { exec: [`${packageDest(m)}/daemon/payload/${a.exec[0]}`, ...a.exec.slice(1)] }),
|
|
178
|
+
} } : {}),
|
|
179
|
+
}) + "\n";
|
|
180
|
+
}
|
|
181
|
+
/** The daemon as the controller runs it: the gateway, with the App as its child. */
|
|
182
|
+
export function gatewayDaemon(daemon) {
|
|
183
|
+
const app = `\${SYNOPKG_PKGDEST}/daemon/payload/${daemon.exec}`;
|
|
184
|
+
return {
|
|
185
|
+
...daemon, exec: GATEWAY_EXEC, args: ["run", "--", app, ...daemon.args],
|
|
186
|
+
...(daemon.uninstall_args ? { uninstall_args: ["exec", "--", app, ...daemon.uninstall_args] } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Builds the gateway from the declared SDK release with the pinned Go, binding its config path. */
|
|
190
|
+
export async function buildIdentityGateway(m, arch, work, run) {
|
|
191
|
+
if (!m.identityGateway)
|
|
192
|
+
return undefined;
|
|
193
|
+
let sdk;
|
|
194
|
+
try {
|
|
195
|
+
sdk = sdkSource(m.identityGateway.sdk);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw new StandaloneBuildError("SDK_DOWNLOAD_FAILED", error.message.replace(/^\[[A-Z_]+\] /, ""));
|
|
199
|
+
}
|
|
200
|
+
const out = join(work, `identity-gateway-${arch}`);
|
|
201
|
+
const ldflags = `-s -w -buildid= -X main.configPath=${packageDest(m)}/${GATEWAY_CONFIG_PATH}`;
|
|
202
|
+
const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, GATEWAY_MAIN], {
|
|
203
|
+
cwd: sdk.goDir, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
|
|
204
|
+
});
|
|
205
|
+
if (result.missing || result.status !== 0) {
|
|
206
|
+
throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_BUILD_FAILED", `identity gateway (SDK ${sdk.version} ${GATEWAY_MAIN}, ${arch}) build failed; that SDK version must contain the gateway. ${PINNED_GO_HINT}\n${result.stderr}`);
|
|
207
|
+
}
|
|
208
|
+
const problem = checkCgiBinary(work, out, arch);
|
|
209
|
+
if (problem)
|
|
210
|
+
throw new StandaloneBuildError(problem.code, problem.message);
|
|
211
|
+
const toolchain = pinnedGoProblem(out);
|
|
212
|
+
if (toolchain)
|
|
213
|
+
throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_BUILD_FAILED", toolchain);
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readComposeApp } from "./compose.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { inspectNativeLayout } from "./native-layout.js";
|
|
3
4
|
import { checkBridgeApp, checkBridgeExecutor } from "./inspect-bridge.js";
|
|
@@ -79,7 +80,10 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
79
80
|
const kind = (expectedKind === "native-go" ? compiled : expectedKind) ?? (paths.has("compose.yaml") ? "container" : binaryBackend ? "binary" : compiled);
|
|
80
81
|
const compiledKind = Object.values(COMPILED_KIND).includes(kind);
|
|
81
82
|
const standalone = compiledKind || kind === "binary";
|
|
82
|
-
const
|
|
83
|
+
const combined = metadata.compose === undefined ? undefined : await recoverCompose(dir, metadata.compose, findings);
|
|
84
|
+
const info = readInfo(join(dir, "INFO"), findings, kind, Boolean(combined));
|
|
85
|
+
if (combined && (!info.os_min_ver || !isDsmAtLeast(info.os_min_ver, CONTAINER_MIN_DSM)))
|
|
86
|
+
findings.push("compose identity_gateway requires DSM >= 7.2.1");
|
|
83
87
|
await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
|
|
84
88
|
const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
|
|
85
89
|
// Before any fixed-lifecycle comparison: those see the SDK steps alone.
|
|
@@ -90,7 +94,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
90
94
|
if (portal && info.os_min_ver && OS_MIN_VER.test(info.os_min_ver) && !isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
|
|
91
95
|
findings.push(`INFO os_min_ver "${info.os_min_ver}" is below the DSM ${PORTAL_MIN_DSM} a portal needs; ${PORTAL_MIN_DSM_REASON}`);
|
|
92
96
|
}
|
|
93
|
-
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal);
|
|
97
|
+
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose);
|
|
94
98
|
checkScripts(dir, findings);
|
|
95
99
|
checkUninstall(dir, findings);
|
|
96
100
|
if (kind === "container")
|
|
@@ -99,7 +103,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
99
103
|
const hasExecutor = (metadata.capabilities ?? []).some(name => name === "rootOps" || name === "delegate" || name === "webapi") || paths.has("executor/executor");
|
|
100
104
|
if (standalone && portal && !hasExecutor)
|
|
101
105
|
findings.push("PORTAL_REQUIRES_EXECUTOR portal needs the executor level (rootOps or delegate): after uninstall only a root purge can stop nginx serving the alias");
|
|
102
|
-
const daemon = standalone ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background) : undefined;
|
|
106
|
+
const daemon = standalone ? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined) : undefined;
|
|
103
107
|
if (background)
|
|
104
108
|
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go");
|
|
105
109
|
let executor;
|
|
@@ -295,7 +299,31 @@ async function checkArch(dir, info, kind, hasCgi, findings) {
|
|
|
295
299
|
findings.push(`INFO arch declares ${declared}, but ${CGI_PAYLOAD_PATH} is a ${actual} executable`);
|
|
296
300
|
}
|
|
297
301
|
}
|
|
298
|
-
async function
|
|
302
|
+
async function recoverCompose(dir, value, findings) {
|
|
303
|
+
try {
|
|
304
|
+
if (!value || typeof value !== 'object' || Object.keys(value).join() !== 'services')
|
|
305
|
+
throw new Error('invalid compose inventory');
|
|
306
|
+
const root = join(dir, 'compose-inspection');
|
|
307
|
+
mkdirSync(root);
|
|
308
|
+
await tar.x({ file: join(dir, 'package.tgz'), cwd: root, filter: p => ['compose/docker-compose.yml', 'sso/gateway.json'].includes(p.replace(/^\.\//, '')) });
|
|
309
|
+
const compose = readComposeApp({ file: 'compose/docker-compose.yml', targets: { x86_64: 'daemon/payload' }, services: value.services }, root, ['x86_64'], e => { throw new Error(e.message); });
|
|
310
|
+
if (!compose)
|
|
311
|
+
throw new Error('invalid compose');
|
|
312
|
+
const config = JSON.parse(readFileSync(join(root, 'sso/gateway.json'), 'utf8'));
|
|
313
|
+
const upstream = /^http:\/\/127\.0\.0\.1:(\d+)$/.exec(config.upstream)?.[1];
|
|
314
|
+
const listen = config.listen;
|
|
315
|
+
if (!upstream || !Object.values(compose.services).some(s => s.port === Number(upstream)) || !listen || !Object.keys(listen).length || Object.entries(listen).some(([key, port]) => !['http', 'https'].includes(key) || !Number.isInteger(port) || Number(port) < 1024 || Number(port) > 65535 || Object.values(compose.services).some(s => s.port === port)))
|
|
316
|
+
throw new Error('invalid gateway network');
|
|
317
|
+
if (config.package !== readInfo(join(dir, 'INFO'), [], 'container').package)
|
|
318
|
+
throw new Error('gateway package mismatch');
|
|
319
|
+
return { compose, identityGateway: { listen } };
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
findings.push(`compose identity gateway contract: ${String(e)}`);
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined) {
|
|
299
327
|
if (![...paths].some(path => path.startsWith("daemon/")))
|
|
300
328
|
return;
|
|
301
329
|
if (info.precheckstartstop !== "yes")
|
|
@@ -309,14 +337,16 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
309
337
|
await tar.x({ file: join(dir, "package.tgz"), cwd: unpacked, filter: path => path.replace(/^\.\//, "") === "daemon/config.json" });
|
|
310
338
|
try {
|
|
311
339
|
const config = JSON.parse(readFileSync(join(unpacked, "daemon/config.json"), "utf8"));
|
|
312
|
-
if (config.package !== info.package || Object.keys(config).some(key => !["package", "exec", "args", "env", "timeouts", "uninstall_args"].includes(key)))
|
|
340
|
+
if (config.package !== info.package || Object.keys(config).some(key => !["package", "exec", "args", "env", "timeouts", "uninstall_args", ...(combined ? ["network"] : [])].includes(key)))
|
|
313
341
|
throw new Error("package/config mismatch");
|
|
314
342
|
const daemon = readDaemon({ exec: config.exec, args: config.args, uninstall_args: config.uninstall_args, env: config.env, timeouts: config.timeouts, targets: { x86_64: "payload" } }, dir, ["x86_64"], () => { });
|
|
315
343
|
if (!daemon)
|
|
316
344
|
throw new Error("invalid config");
|
|
345
|
+
if (combined && (daemon.exec !== "_sso/gateway" || JSON.stringify(daemon.args) !== '["run"]' || daemon.uninstall_args))
|
|
346
|
+
throw new Error("compose gateway must run without a child");
|
|
317
347
|
if (payload.find(p => p.path === `daemon/payload/${daemon.exec}`)?.mode !== "0755")
|
|
318
348
|
throw new Error("missing executable");
|
|
319
|
-
const manifest = { package: info.package, daemon, capabilities: { ...(executor ? { rootOps: true } : {}), ...(background ? { jobs: { worker: 'sha256-v1' } } : {}) } };
|
|
349
|
+
const manifest = { package: info.package, daemon, ...combined, capabilities: { ...(executor ? { rootOps: true } : {}), ...(background ? { jobs: { worker: 'sha256-v1' } } : {}) } };
|
|
320
350
|
// A config from before timeouts.uninstall pairs with a control that rejects the key.
|
|
321
351
|
const legacy = typeof config.timeouts === "object" && config.timeouts !== null && !("uninstall" in config.timeouts);
|
|
322
352
|
const { uninstall: _, ...legacyTimeouts } = daemon.timeouts;
|
|
@@ -333,7 +363,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
333
363
|
findings.push("daemon/config.json or the payload does not match the fixed daemon contract");
|
|
334
364
|
}
|
|
335
365
|
}
|
|
336
|
-
function readInfo(path, findings, kind) {
|
|
366
|
+
function readInfo(path, findings, kind, compose = false) {
|
|
337
367
|
if (!existsSync(path)) {
|
|
338
368
|
findings.push("INFO is missing");
|
|
339
369
|
return {};
|
|
@@ -354,7 +384,7 @@ function readInfo(path, findings, kind) {
|
|
|
354
384
|
findings.push("INFO must have thirdparty=\"yes\" and dsmuidir=\"ui\" for DSM to serve /webman/3rdparty/<package>/");
|
|
355
385
|
}
|
|
356
386
|
// A container needs Container Manager (and Web Station for a portal); a Node App the Node.js it runs on.
|
|
357
|
-
const allowed = kind === "container" ? /^(WebStation|ContainerManager)([><=].*)?$/ : kind === "native-node" ? /^Node\.js_v[0-9]{2}([><=].*)?$/ : undefined;
|
|
387
|
+
const allowed = compose ? /^ContainerManager$/ : kind === "container" ? /^(WebStation|ContainerManager)([><=].*)?$/ : kind === "native-node" ? /^Node\.js_v[0-9]{2}([><=].*)?$/ : undefined;
|
|
358
388
|
if (info.install_dep_packages && !(allowed && info.install_dep_packages.split(":").every(dep => allowed.test(dep)))) {
|
|
359
389
|
findings.push(`INFO depends on other packages: ${info.install_dep_packages}`);
|
|
360
390
|
}
|
|
@@ -379,10 +409,10 @@ async function checkContainerExposure(dir, packageTgz, findings) {
|
|
|
379
409
|
function readOptional(path) {
|
|
380
410
|
return existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
381
411
|
}
|
|
382
|
-
function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal) {
|
|
412
|
+
function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose) {
|
|
383
413
|
const confDir = join(dir, "conf");
|
|
384
414
|
const entries = existsSync(confDir) ? readdirSync(confDir) : [];
|
|
385
|
-
const nativeResource = kind !== "container" ? renderStandaloneResource({ package: pkg ?? "", capabilities: background ? { jobs: { worker: "sha256-v1" } } : {}, ...(sharedFolder ? { sharedFolder } : {}) }, portal) : undefined;
|
|
415
|
+
const nativeResource = kind !== "container" ? renderStandaloneResource({ package: pkg ?? "", ...(compose ? { compose } : {}), capabilities: background ? { jobs: { worker: "sha256-v1" } } : {}, ...(sharedFolder ? { sharedFolder } : {}) }, portal) : undefined;
|
|
386
416
|
if (nativeResource !== undefined && readOptional(join(confDir, "resource")) !== nativeResource) {
|
|
387
417
|
findings.push(`conf/resource must be ${nativeResource} (only the official data-share, systemd-user-unit and web-config, and consistent with the install.sharedFolder and portal declarations)`);
|
|
388
418
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** At most this many App values, each at most this long, reach the App from one launch. */
|
|
2
|
+
export declare const MAX_LAUNCH_PARAMS = 32;
|
|
3
|
+
export declare const MAX_LAUNCH_VALUE = 2048;
|
|
4
|
+
/**
|
|
5
|
+
* `launchParams(param)`: the App's own values from the param DSM hands the
|
|
6
|
+
* window's onOpen and onRequest, a `launchParam` desktop URL decoded by DSM
|
|
7
|
+
* into an object or AppLaunch's second argument. Only short single-line string
|
|
8
|
+
* values under plain keys pass; the result is a frozen, prototype-free copy.
|
|
9
|
+
* The values stay untrusted text for the App.
|
|
10
|
+
*/
|
|
11
|
+
export declare const LAUNCH_PARAMS_RUNTIME: string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** At most this many App values, each at most this long, reach the App from one launch. */
|
|
2
|
+
export const MAX_LAUNCH_PARAMS = 32;
|
|
3
|
+
export const MAX_LAUNCH_VALUE = 2048;
|
|
4
|
+
/**
|
|
5
|
+
* DSM's own string keys in a launch: AppLaunch reads className and
|
|
6
|
+
* instanceName, AppWindow.onOpen sets the window title from title, and desktop
|
|
7
|
+
* restore replays openConfig with dsm_version. Its other keys (fromRestore,
|
|
8
|
+
* windowState, cms_id, ...) are not strings and are left out with the rest.
|
|
9
|
+
*/
|
|
10
|
+
const DSM_KEYS = ["className", "instanceName", "dsm_version", "title"];
|
|
11
|
+
/**
|
|
12
|
+
* `launchParams(param)`: the App's own values from the param DSM hands the
|
|
13
|
+
* window's onOpen and onRequest, a `launchParam` desktop URL decoded by DSM
|
|
14
|
+
* into an object or AppLaunch's second argument. Only short single-line string
|
|
15
|
+
* values under plain keys pass; the result is a frozen, prototype-free copy.
|
|
16
|
+
* The values stay untrusted text for the App.
|
|
17
|
+
*/
|
|
18
|
+
export const LAUNCH_PARAMS_RUNTIME = `
|
|
19
|
+
function launchParams(param) {
|
|
20
|
+
var dsmKeys = ${JSON.stringify(DSM_KEYS)};
|
|
21
|
+
var params = Object.create(null), count = 0;
|
|
22
|
+
if (param && typeof param === "object" && !Array.isArray(param)) {
|
|
23
|
+
Object.keys(param).forEach(function (key) {
|
|
24
|
+
var value = param[key];
|
|
25
|
+
if (count >= ${MAX_LAUNCH_PARAMS} || dsmKeys.indexOf(key) >= 0 || !/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(key)) return;
|
|
26
|
+
if (typeof value !== "string" || value.length > ${MAX_LAUNCH_VALUE} || /[\\u0000-\\u001f\\u007f]/.test(value)) return;
|
|
27
|
+
params[key] = value;
|
|
28
|
+
count++;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze(params);
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { composeDaemon, renderComposeInstall } from "./compose.js";
|
|
1
2
|
import { renderBackgroundControl, renderBackgroundWorkerStart, renderBackgroundWorkerStop, renderUserManagerRecovery, backgroundUnitName } from "./background.js";
|
|
2
3
|
import { renderPackagePostuninst } from "../build-spk/native-lifecycle.js";
|
|
3
4
|
import { renderSharedFolderScript } from "./shared-folder.js";
|
|
@@ -9,6 +10,11 @@ import { composeAppScript } from "./app-scripts.js";
|
|
|
9
10
|
const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_uninstall_hook\n';
|
|
10
11
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
11
12
|
export function renderStandaloneScript(name, manifest) {
|
|
13
|
+
if (manifest.compose) {
|
|
14
|
+
const native = { ...manifest, compose: undefined, daemon: composeDaemon(manifest) };
|
|
15
|
+
const body = name === "preinst" ? renderComposeInstall(manifest.package) : renderStandaloneScript(name, { ...native, appScripts: undefined });
|
|
16
|
+
return manifest.appScripts?.some(app => app === name) ? composeAppScript(name, body) : body;
|
|
17
|
+
}
|
|
12
18
|
const portal = nativePortal(manifest);
|
|
13
19
|
const script = renderCapabilityScript(name, manifest);
|
|
14
20
|
const sdk = portal ? script.replace("#!/bin/sh\n", () => `#!/bin/sh\n${renderPortalLifecycle(name, portal)}`) : script;
|
|
@@ -7,7 +7,9 @@ import { type Catalogs, type InfoTranslations } from "./i18n.js";
|
|
|
7
7
|
export type { InfoText } from "./i18n.js";
|
|
8
8
|
import { type RootOp } from "./root-ops.js";
|
|
9
9
|
import type { RootSettings } from "../runtime/root-programs.js";
|
|
10
|
+
import { type ComposeApp } from "./compose.js";
|
|
10
11
|
import { type Daemon } from "./daemon.js";
|
|
12
|
+
import { type IdentityGateway } from "./identity-gateway.js";
|
|
11
13
|
import { type Uninstall } from "./uninstall.js";
|
|
12
14
|
import { type AppScriptName } from "./app-scripts.js";
|
|
13
15
|
import { type Background } from "./background.js";
|
|
@@ -76,6 +78,9 @@ export interface StandaloneManifest {
|
|
|
76
78
|
tasks: ExecutorTask[];
|
|
77
79
|
};
|
|
78
80
|
daemon?: Daemon;
|
|
81
|
+
/** `identity_gateway`: signs DSM users in to the daemon's own web UI. */
|
|
82
|
+
identityGateway?: IdentityGateway;
|
|
83
|
+
compose?: ComposeApp;
|
|
79
84
|
/** `uninstall`: the App's own purge hook and the state the wizard may delete. */
|
|
80
85
|
uninstall?: Uninstall;
|
|
81
86
|
/** `install.sharedFolder`: the Shared Folder the install wizard selects or `data-share` creates. */
|
|
@@ -7,7 +7,9 @@ import { parse } from "yaml";
|
|
|
7
7
|
import { isNativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
8
8
|
import { checkGlossary, loadCatalogs, readInfoTranslations } from "./i18n.js";
|
|
9
9
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR } from "./root-ops.js";
|
|
10
|
+
import { readComposeApp } from "./compose.js";
|
|
10
11
|
import { readDaemon } from "./daemon.js";
|
|
12
|
+
import { readIdentityGateway } from "./identity-gateway.js";
|
|
11
13
|
import { readUninstall } from "./uninstall.js";
|
|
12
14
|
import { readAppScripts } from "./app-scripts.js";
|
|
13
15
|
import { readBackground } from "./background.js";
|
|
@@ -31,7 +33,7 @@ import { BRIDGE_RUNTIMES, isBridgeBackend, overlaps, readBridgeBackend } from ".
|
|
|
31
33
|
export const STANDALONE_MANIFEST = "dsm-package.yaml";
|
|
32
34
|
export const KNOWN_FIELDS = new Set([
|
|
33
35
|
"schema", "package", "version", "build", "displayname", "description",
|
|
34
|
-
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "uninstall", "entry", "portal", "icon", "reproducible", "root", "login_style",
|
|
36
|
+
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "identity_gateway", "compose", "install", "uninstall", "entry", "portal", "icon", "reproducible", "root", "login_style",
|
|
35
37
|
]);
|
|
36
38
|
const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
|
|
37
39
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
@@ -83,7 +85,7 @@ export function loadStandaloneManifest(root) {
|
|
|
83
85
|
return { ok: false, errors: [{ code: "STANDALONE_MANIFEST_YAML", message: String(error) }] };
|
|
84
86
|
}
|
|
85
87
|
const c = new Collector(root);
|
|
86
|
-
if (existsSync(join(root, "compose.yaml")))
|
|
88
|
+
if (existsSync(join(root, "compose.yaml")) && !(isRecord(raw) && isRecord(raw.compose) && raw.compose.file === "compose.yaml"))
|
|
87
89
|
c.fail("PROJECT_RUNTIME_EXCLUSIVE", "compose.yaml and dsm-package.yaml are mutually exclusive; keep only one.");
|
|
88
90
|
if (!isRecord(raw)) {
|
|
89
91
|
c.fail("STANDALONE_MANIFEST_YAML", `${STANDALONE_MANIFEST} must be a YAML object.`);
|
|
@@ -170,6 +172,12 @@ function readManifest(raw, c) {
|
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
const daemon = arches && readDaemon(raw.daemon, c.root, arches, e => c.fail(e.code, e.message));
|
|
175
|
+
const compose = arches && readComposeApp(raw.compose, c.root, arches, e => c.fail(e.code, e.message));
|
|
176
|
+
if (raw.compose !== undefined && (raw.daemon !== undefined || raw.identity_gateway === undefined || Object.keys(capabilities).length || !osMinVer || !isDsmAtLeast(osMinVer, "7.2.1-69057")))
|
|
177
|
+
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose requires identity_gateway, DSM >= 7.2.1, no daemon and no capabilities.");
|
|
178
|
+
const identityGateway = readIdentityGateway(raw.identity_gateway, daemon || (compose ? { targets: compose.targets } : undefined), e => c.fail(e.code, e.message));
|
|
179
|
+
if (compose && identityGateway && (!Object.values(compose.services).some(s => s.port === identityGateway.upstream) || Object.values(compose.services).some(s => Object.values(identityGateway.listen).includes(s.port) || s.port === identityGateway.oidc?.issuer_port)))
|
|
180
|
+
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose service ports must contain upstream and not overlap gateway or issuer ports.");
|
|
173
181
|
if (capabilities.jobs && raw.daemon !== undefined && capabilities.delegate?.lifetime !== 'until-revoked') {
|
|
174
182
|
c.fail("CAPABILITY_COMBINATION_INVALID", "daemon can only coexist with an explicit until-revoked delegate fixed worker; plain capabilities.jobs stays mutually exclusive.");
|
|
175
183
|
}
|
|
@@ -261,6 +269,8 @@ function readManifest(raw, c) {
|
|
|
261
269
|
...(rootOps ? { rootOps } : {}),
|
|
262
270
|
...(rootPrograms ? { rootPrograms } : {}),
|
|
263
271
|
...(daemon ? { daemon } : {}),
|
|
272
|
+
...(identityGateway ? { identityGateway } : {}),
|
|
273
|
+
...(compose ? { compose } : {}),
|
|
264
274
|
...(uninstall ? { uninstall } : {}),
|
|
265
275
|
...(sharedFolder ? { sharedFolder } : {}),
|
|
266
276
|
...(entry ? { entry } : {}),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** Wrapper-owned subscriptions; no dependency on native event names or Ext. */
|
|
2
|
-
export declare const REOPEN_EVENTS_RUNTIME = "\nfunction createReopenEvents() {\n var active = true, handlers = [];\n function subscribe(fn) {\n if (!active || typeof fn !== \"function\") return function () {};\n function handler() { if (active && fn) fn(); }\n function unsubscribe() {\n if (!fn) return;\n fn = null;\n var index = handlers.indexOf(handler);\n if (index !== -1) handlers.splice(index, 1);\n }\n handler.unsubscribe = unsubscribe;\n handlers.push(handler);\n return unsubscribe;\n }\n return {\n handlers: handlers,\n subscribe: subscribe,\n emit: function () { handlers.slice().forEach(function (handler) { handler(); }); },\n dispose: function () {\n active = false;\n handlers.slice().forEach(function (handler) { handler.unsubscribe(); });\n }\n };\n}\n";
|
|
1
|
+
/** Wrapper-owned subscriptions; no dependency on native event names or Ext. emit(value) hands value to each fn. */
|
|
2
|
+
export declare const REOPEN_EVENTS_RUNTIME = "\nfunction createReopenEvents() {\n var active = true, handlers = [];\n function subscribe(fn) {\n if (!active || typeof fn !== \"function\") return function () {};\n function handler(value) { if (active && fn) fn(value); }\n function unsubscribe() {\n if (!fn) return;\n fn = null;\n var index = handlers.indexOf(handler);\n if (index !== -1) handlers.splice(index, 1);\n }\n handler.unsubscribe = unsubscribe;\n handlers.push(handler);\n return unsubscribe;\n }\n return {\n handlers: handlers,\n subscribe: subscribe,\n emit: function (value) { handlers.slice().forEach(function (handler) { handler(value); }); },\n dispose: function () {\n active = false;\n handlers.slice().forEach(function (handler) { handler.unsubscribe(); });\n }\n };\n}\n";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/** Wrapper-owned subscriptions; no dependency on native event names or Ext. */
|
|
1
|
+
/** Wrapper-owned subscriptions; no dependency on native event names or Ext. emit(value) hands value to each fn. */
|
|
2
2
|
export const REOPEN_EVENTS_RUNTIME = `
|
|
3
3
|
function createReopenEvents() {
|
|
4
4
|
var active = true, handlers = [];
|
|
5
5
|
function subscribe(fn) {
|
|
6
6
|
if (!active || typeof fn !== "function") return function () {};
|
|
7
|
-
function handler() { if (active && fn) fn(); }
|
|
7
|
+
function handler(value) { if (active && fn) fn(value); }
|
|
8
8
|
function unsubscribe() {
|
|
9
9
|
if (!fn) return;
|
|
10
10
|
fn = null;
|
|
@@ -18,7 +18,7 @@ function createReopenEvents() {
|
|
|
18
18
|
return {
|
|
19
19
|
handlers: handlers,
|
|
20
20
|
subscribe: subscribe,
|
|
21
|
-
emit: function () { handlers.slice().forEach(function (handler) { handler(); }); },
|
|
21
|
+
emit: function (value) { handlers.slice().forEach(function (handler) { handler(value); }); },
|
|
22
22
|
dispose: function () {
|
|
23
23
|
active = false;
|
|
24
24
|
handlers.slice().forEach(function (handler) { handler.unsubscribe(); });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ComposeApp } from "./compose.js";
|
|
1
2
|
import type { Capabilities } from "./manifest.js";
|
|
2
3
|
import { type SharedFolder } from "./shared-folder.js";
|
|
3
4
|
import { type PortalPackage } from "../portal/package.js";
|
|
@@ -9,5 +10,6 @@ import { type PortalPackage } from "../portal/package.js";
|
|
|
9
10
|
export declare function renderStandaloneResource(m: {
|
|
10
11
|
package: string;
|
|
11
12
|
capabilities: Capabilities;
|
|
13
|
+
compose?: ComposeApp;
|
|
12
14
|
sharedFolder?: SharedFolder;
|
|
13
15
|
}, portal?: PortalPackage): string | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderDockerProjectResource } from "../build-spk/resource-worker.js";
|
|
1
2
|
import { sharedFolderResource } from "./shared-folder.js";
|
|
2
3
|
import { portalResource } from "../portal/package.js";
|
|
3
4
|
/**
|
|
@@ -8,6 +9,7 @@ import { portalResource } from "../portal/package.js";
|
|
|
8
9
|
export function renderStandaloneResource(m, portal) {
|
|
9
10
|
const dataShare = m.sharedFolder && sharedFolderResource(m.package, m.sharedFolder);
|
|
10
11
|
const resource = {
|
|
12
|
+
...(m.compose ? renderDockerProjectResource(m.package, "compose", false) : {}),
|
|
11
13
|
...(dataShare ? { "data-share": dataShare } : {}),
|
|
12
14
|
...(m.capabilities.jobs ? { "systemd-user-unit": {} } : {}),
|
|
13
15
|
...(portal ? portalResource() : {}),
|
package/dist/standalone/spk.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface AssembleOptions {
|
|
|
15
15
|
outputPath: string;
|
|
16
16
|
executor?: ExecutorArtifact;
|
|
17
17
|
daemonControl?: string;
|
|
18
|
+
/** The SDK identity gateway, required exactly when the manifest declares identity_gateway. */
|
|
19
|
+
identityGateway?: string;
|
|
18
20
|
/** The compiled background worker, required exactly when capabilities.jobs is enabled. */
|
|
19
21
|
backgroundWorker?: string;
|
|
20
22
|
/** A Python, Node or Rust App's finished directory, installed as target/app. */
|