@synopackageland/cli 0.14.1 → 0.15.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 +20 -1
- package/README.md +33 -18
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -846
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/unsupported-types.js +1 -1
- package/dist/contract/validate.js +2 -2
- package/dist/contract/web-launch.d.ts +1 -12
- package/dist/contract/web-launch.js +10 -74
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/e2e/dsm.d.ts +2 -2
- package/dist/e2e/dsm.js +10 -6
- package/dist/error-registry.js +1020 -834
- package/dist/host/share-permission-lookup.d.ts +1 -1
- package/dist/host/share-permission.d.ts +1 -1
- package/dist/host/share-permission.js +1 -1
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +5 -5
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +39 -8
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +12 -0
- package/dist/standalone/inspect-spk.js +666 -136
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +8 -1
- package/dist/standalone/manifest.js +303 -86
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +138 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +4 -2
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +9 -18
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +5 -1
- package/skills/reference/identity-gateway.md +8 -144
- package/skills/reference/wrap.md +10 -39
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, lstatSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from "../build-spk/go-toolchain.js";
|
|
3
|
+
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem, } from "../build-spk/go-toolchain.js";
|
|
4
4
|
import { goChildEnv } from "../runtime/go-env.js";
|
|
5
5
|
import { sdkSource } from "../runtime/sdk-source.js";
|
|
6
6
|
import { checkCgiBinary } from "./binary.js";
|
|
7
|
+
import { isWebAccessMode, DSM_GROUP_NAME, } from "../contract/web-launch.js";
|
|
7
8
|
import { relativeExec, token } from "./daemon.js";
|
|
8
9
|
import { StandaloneBuildError } from "./spk.js";
|
|
9
10
|
/** The SDK's identity gateway: the daemon wrapper and the DSM-origin authorize CGI. */
|
|
@@ -15,27 +16,39 @@ export const GATEWAY_PAYLOAD_DIR = "_sso";
|
|
|
15
16
|
export const GATEWAY_EXEC = `${GATEWAY_PAYLOAD_DIR}/gateway`;
|
|
16
17
|
export const GATEWAY_CONFIG_PATH = "sso/gateway.json";
|
|
17
18
|
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 only = (v, keys) => Object.keys(v).every((k) => keys.includes(k));
|
|
19
20
|
const port = (v) => Number.isInteger(v) && v > 0 && v < 65536;
|
|
20
21
|
/** An absolute local path without dot segments, backslashes or a query. */
|
|
21
|
-
const localPath = (v) => typeof v === "string" &&
|
|
22
|
-
|
|
22
|
+
const localPath = (v) => typeof v === "string" &&
|
|
23
|
+
v.length <= 512 &&
|
|
24
|
+
/^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/.test(v) &&
|
|
25
|
+
!v.startsWith("//") &&
|
|
26
|
+
!v.split("/").some((p) => p === "." || p === "..");
|
|
23
27
|
const strings = (v, ok, max = 64) => Array.isArray(v) && v.length <= max && v.every(ok);
|
|
24
28
|
const name = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(v);
|
|
25
29
|
const headerName = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(v);
|
|
26
30
|
const claimValue = (v) => typeof v === "string" && /^[A-Za-z0-9_.:-]{1,64}$/.test(v);
|
|
27
31
|
/** A mobile app's custom-scheme redirect; web schemes are same-origin redirect_paths instead. */
|
|
28
|
-
const nativeRedirect = (v) => typeof v === "string" &&
|
|
29
|
-
&&
|
|
32
|
+
const nativeRedirect = (v) => typeof v === "string" &&
|
|
33
|
+
v.length <= 512 &&
|
|
34
|
+
/^[a-z][a-z0-9+.-]*:[^\s]*$/.test(v) &&
|
|
35
|
+
!/^(https?|javascript|data|vbscript|file|blob|about):/.test(v);
|
|
30
36
|
const DSM_PORTS = [5000, 5001];
|
|
31
37
|
/** 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}");
|
|
38
|
+
const placeholders = (v) => (v.match(/\{[^{}]*\}/g) ?? []).every((p) => p === "{uid}" || p === "{name}");
|
|
33
39
|
/** A clean path under the package var directory that names the account, outside the gateway's sso/. */
|
|
34
|
-
const removePath = (v) => typeof v === "string" &&
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
const removePath = (v) => typeof v === "string" &&
|
|
41
|
+
v.length <= 512 &&
|
|
42
|
+
placeholders(v) &&
|
|
43
|
+
/^[A-Za-z0-9_{}][A-Za-z0-9_.{}-]*(\/[A-Za-z0-9_.{}-]+)*$/.test(v) &&
|
|
44
|
+
!v.split("/").some((p) => p === "." || p === "..") &&
|
|
45
|
+
v.split("/")[0] !== "sso" &&
|
|
46
|
+
/\{(uid|name)\}/.test(v);
|
|
37
47
|
function readAccount(v, daemon) {
|
|
38
|
-
if (!record(v) ||
|
|
48
|
+
if (!record(v) ||
|
|
49
|
+
!only(v, ["on_delete"]) ||
|
|
50
|
+
!Array.isArray(v.on_delete) ||
|
|
51
|
+
v.on_delete.length > 16)
|
|
39
52
|
return "account accepts on_delete, a list of at most 16 actions.";
|
|
40
53
|
const actions = [];
|
|
41
54
|
for (const a of v.on_delete) {
|
|
@@ -43,11 +56,16 @@ function readAccount(v, daemon) {
|
|
|
43
56
|
actions.push({ remove: a.remove });
|
|
44
57
|
continue;
|
|
45
58
|
}
|
|
46
|
-
if (!record(a) ||
|
|
59
|
+
if (!record(a) ||
|
|
60
|
+
!only(a, ["exec"]) ||
|
|
61
|
+
!Array.isArray(a.exec) ||
|
|
62
|
+
!relativeExec(a.exec[0]) ||
|
|
63
|
+
!strings(a.exec.slice(1), (s) => token(s) && placeholders(s))) {
|
|
47
64
|
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
65
|
}
|
|
49
66
|
const argv = a.exec;
|
|
50
|
-
if (!Object.
|
|
67
|
+
if (!Object.keys(daemon.targets).length ||
|
|
68
|
+
!Object.values(daemon.targets).every((dir) => existsSync(join(dir, argv[0]))))
|
|
51
69
|
return `account.on_delete exec ${argv[0]} is not in the daemon payload.`;
|
|
52
70
|
actions.push({ exec: argv });
|
|
53
71
|
}
|
|
@@ -58,88 +76,210 @@ function readRoles(v) {
|
|
|
58
76
|
return undefined;
|
|
59
77
|
if (!record(v) || !only(v, ["claim", "admin", "user"]))
|
|
60
78
|
return false;
|
|
61
|
-
const roles = {
|
|
62
|
-
|
|
79
|
+
const roles = {
|
|
80
|
+
claim: v.claim ?? "groups",
|
|
81
|
+
admin: v.admin ?? "admin",
|
|
82
|
+
...(v.user !== undefined ? { user: v.user } : {}),
|
|
83
|
+
};
|
|
84
|
+
return claimValue(roles.claim) &&
|
|
85
|
+
claimValue(roles.admin) &&
|
|
86
|
+
(roles.user === undefined || claimValue(roles.user))
|
|
87
|
+
? roles
|
|
88
|
+
: false;
|
|
63
89
|
}
|
|
64
90
|
function readClient(v) {
|
|
65
|
-
if (!record(v) ||
|
|
91
|
+
if (!record(v) ||
|
|
92
|
+
!only(v, ["id", "redirect_paths", "native_redirects", "pkce", "roles"]))
|
|
66
93
|
return undefined;
|
|
67
94
|
const native = v.native_redirects ?? [], roles = readRoles(v.roles);
|
|
68
|
-
if (!name(v.id) ||
|
|
95
|
+
if (!name(v.id) ||
|
|
96
|
+
!strings(v.redirect_paths, localPath) ||
|
|
97
|
+
!strings(native, nativeRedirect) ||
|
|
98
|
+
(v.pkce !== undefined && typeof v.pkce !== "boolean") ||
|
|
99
|
+
roles === false)
|
|
69
100
|
return undefined;
|
|
70
101
|
if (!v.redirect_paths.length && !native.length)
|
|
71
102
|
return undefined;
|
|
72
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
id: v.id,
|
|
105
|
+
redirect_paths: v.redirect_paths,
|
|
106
|
+
native_redirects: native,
|
|
107
|
+
require_pkce: v.pkce === true,
|
|
108
|
+
...(roles ? { roles } : {}),
|
|
109
|
+
};
|
|
73
110
|
}
|
|
74
|
-
export function readIdentityGateway(value, daemon, fail) {
|
|
111
|
+
export function readIdentityGateway(value, daemon, fail, packagedPayload = false, composeBridge = false) {
|
|
75
112
|
if (value === undefined)
|
|
76
113
|
return undefined;
|
|
77
|
-
const invalid = (why) => {
|
|
114
|
+
const invalid = (why) => {
|
|
115
|
+
fail({
|
|
116
|
+
code: "STANDALONE_IDENTITY_GATEWAY_INVALID",
|
|
117
|
+
message: `identity_gateway: ${why}`,
|
|
118
|
+
});
|
|
119
|
+
return undefined;
|
|
120
|
+
};
|
|
78
121
|
if (!daemon)
|
|
79
122
|
return invalid("needs daemon (native child) or compose (Container Manager upstream).");
|
|
80
|
-
if (!record(value) ||
|
|
123
|
+
if (!record(value) ||
|
|
124
|
+
!only(value, [
|
|
125
|
+
"sdk",
|
|
126
|
+
"mode",
|
|
127
|
+
"access",
|
|
128
|
+
"groups",
|
|
129
|
+
"max_upload_bytes",
|
|
130
|
+
"response_header_timeout_seconds",
|
|
131
|
+
"upstream",
|
|
132
|
+
"listen",
|
|
133
|
+
"passthrough",
|
|
134
|
+
"redirects",
|
|
135
|
+
"email_domain",
|
|
136
|
+
"session_ttl",
|
|
137
|
+
"roles",
|
|
138
|
+
"headers",
|
|
139
|
+
"oidc",
|
|
140
|
+
"token",
|
|
141
|
+
"account",
|
|
142
|
+
])) {
|
|
81
143
|
return invalid("accepts only sdk, mode, upstream, listen, passthrough, redirects, email_domain, session_ttl, roles, headers, oidc, token and account.");
|
|
82
144
|
}
|
|
83
145
|
const v = value;
|
|
146
|
+
const access = v.access ?? "dsm-users";
|
|
147
|
+
if (!isWebAccessMode(access) || (access === "public" && v.mode !== "none"))
|
|
148
|
+
return invalid("access must be dsm-users, dsm-admins, dsm-groups or public (none mode only).");
|
|
149
|
+
if (access === "dsm-groups" &&
|
|
150
|
+
(!Array.isArray(v.groups) ||
|
|
151
|
+
!v.groups.length ||
|
|
152
|
+
!v.groups.every((g) => typeof g === "string" && DSM_GROUP_NAME.test(g))))
|
|
153
|
+
return invalid("dsm-groups requires a nonempty exact groups allowlist.");
|
|
154
|
+
if (v.groups !== undefined && access !== "dsm-groups")
|
|
155
|
+
return invalid("groups applies only to access: dsm-groups.");
|
|
156
|
+
if (v.max_upload_bytes !== undefined &&
|
|
157
|
+
(!Number.isSafeInteger(v.max_upload_bytes) ||
|
|
158
|
+
Number(v.max_upload_bytes) < 1 ||
|
|
159
|
+
Number(v.max_upload_bytes) > 16 * 1024 ** 3))
|
|
160
|
+
return invalid("max_upload_bytes must be 1–16 GiB.");
|
|
161
|
+
if (v.response_header_timeout_seconds !== undefined &&
|
|
162
|
+
(!Number.isInteger(v.response_header_timeout_seconds) ||
|
|
163
|
+
Number(v.response_header_timeout_seconds) < 1 ||
|
|
164
|
+
Number(v.response_header_timeout_seconds) > 3600))
|
|
165
|
+
return invalid("response_header_timeout_seconds must be 1–3600.");
|
|
84
166
|
if (typeof v.sdk !== "string" || !/^\d+\.\d+\.\d+$/.test(v.sdk))
|
|
85
167
|
return invalid("sdk must be the SDK release (x.y.z) to build the gateway from.");
|
|
86
|
-
if (v.mode !== "
|
|
87
|
-
|
|
168
|
+
if (v.mode !== "none" &&
|
|
169
|
+
v.mode !== "oidc" &&
|
|
170
|
+
v.mode !== "header" &&
|
|
171
|
+
v.mode !== "token")
|
|
172
|
+
return invalid("mode must be none, oidc, header or token.");
|
|
88
173
|
if (!port(v.upstream))
|
|
89
174
|
return invalid("upstream must be the App's loopback port.");
|
|
90
175
|
const listen = v.listen;
|
|
91
|
-
if (!record(listen) ||
|
|
92
|
-
|
|
176
|
+
if (!record(listen) ||
|
|
177
|
+
!only(listen, ["http", "https"]) ||
|
|
178
|
+
(listen.http === undefined && listen.https === undefined) ||
|
|
179
|
+
![listen.http, listen.https].every((p) => p === undefined || port(p)))
|
|
93
180
|
return invalid("listen needs an http and/or https port.");
|
|
94
181
|
const issuer = record(v.oidc) ? v.oidc.issuer_port : undefined;
|
|
95
|
-
const ports = [
|
|
96
|
-
|
|
182
|
+
const ports = [
|
|
183
|
+
listen.http,
|
|
184
|
+
listen.https,
|
|
185
|
+
...(composeBridge ? [] : [v.upstream]),
|
|
186
|
+
issuer,
|
|
187
|
+
].filter((p) => p !== undefined);
|
|
188
|
+
if (new Set(ports).size !== ports.length ||
|
|
189
|
+
ports.some((p) => DSM_PORTS.includes(p)))
|
|
97
190
|
return invalid("listen, upstream and oidc.issuer_port must be distinct and not DSM's 5000/5001.");
|
|
98
191
|
const passthrough = v.passthrough ?? [];
|
|
99
|
-
if (!strings(passthrough, p => localPath(p) && p !== "/" && !/^\/_sso(\/|$)/.test(p)))
|
|
192
|
+
if (!strings(passthrough, (p) => localPath(p) && p !== "/" && !/^\/_sso(\/|$)/.test(p)))
|
|
100
193
|
return invalid("passthrough must be path prefixes such as /share/, outside /_sso.");
|
|
101
194
|
const redirects = v.redirects ?? {};
|
|
102
|
-
if (!record(redirects) ||
|
|
195
|
+
if (!record(redirects) ||
|
|
196
|
+
Object.entries(redirects).some(([from, to]) => !localPath(from) || !localPath(to)))
|
|
103
197
|
return invalid("redirects maps local paths to local paths.");
|
|
104
|
-
if (v.email_domain !== undefined &&
|
|
198
|
+
if (v.email_domain !== undefined &&
|
|
199
|
+
(typeof v.email_domain !== "string" ||
|
|
200
|
+
!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/.test(v.email_domain)))
|
|
105
201
|
return invalid("email_domain must be a domain name.");
|
|
106
|
-
if (v.session_ttl !== undefined &&
|
|
202
|
+
if (v.session_ttl !== undefined &&
|
|
203
|
+
(!Number.isInteger(v.session_ttl) ||
|
|
204
|
+
v.session_ttl < 60 ||
|
|
205
|
+
v.session_ttl > 86400))
|
|
107
206
|
return invalid("session_ttl must be 60-86400 seconds.");
|
|
108
207
|
const roles = readRoles(v.roles);
|
|
109
208
|
if (roles === false)
|
|
110
209
|
return invalid("roles accepts claim, admin and user values.");
|
|
111
210
|
const headers = v.headers;
|
|
112
|
-
if (headers !== undefined &&
|
|
211
|
+
if (headers !== undefined &&
|
|
212
|
+
(!record(headers) ||
|
|
213
|
+
!only(headers, ["user", "email", "name", "groups", "uid"]) ||
|
|
214
|
+
!Object.values(headers).every(headerName))) {
|
|
113
215
|
return invalid("headers maps user, email, name, groups and uid to header names.");
|
|
114
216
|
}
|
|
115
217
|
if (v.mode === "header" && !(record(headers) && headers.user))
|
|
116
218
|
return invalid("header mode needs headers.user.");
|
|
117
219
|
const gateway = {
|
|
118
|
-
sdk: v.sdk,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
...(
|
|
220
|
+
sdk: v.sdk,
|
|
221
|
+
mode: v.mode,
|
|
222
|
+
access,
|
|
223
|
+
...(Array.isArray(v.groups) ? { groups: v.groups } : {}),
|
|
224
|
+
...(v.max_upload_bytes !== undefined
|
|
225
|
+
? { max_upload_bytes: v.max_upload_bytes }
|
|
226
|
+
: {}),
|
|
227
|
+
...(v.response_header_timeout_seconds !== undefined
|
|
228
|
+
? {
|
|
229
|
+
response_header_timeout_seconds: v.response_header_timeout_seconds,
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
232
|
+
upstream: v.upstream,
|
|
233
|
+
listen: {
|
|
234
|
+
...(listen.http ? { http: listen.http } : {}),
|
|
235
|
+
...(listen.https ? { https: listen.https } : {}),
|
|
236
|
+
},
|
|
237
|
+
passthrough: passthrough,
|
|
238
|
+
redirects: redirects,
|
|
239
|
+
...(v.email_domain ? { email_domain: v.email_domain } : {}),
|
|
240
|
+
...(v.session_ttl ? { session_ttl: v.session_ttl } : {}),
|
|
241
|
+
...(roles ? { roles } : {}),
|
|
242
|
+
...(headers ? { headers: headers } : {}),
|
|
122
243
|
};
|
|
123
244
|
if (v.mode === "oidc") {
|
|
124
245
|
const o = v.oidc;
|
|
125
|
-
if (!record(o) ||
|
|
246
|
+
if (!record(o) ||
|
|
247
|
+
!only(o, ["issuer_port", "clients", "rewrite_json"]) ||
|
|
248
|
+
!port(o.issuer_port) ||
|
|
249
|
+
!Array.isArray(o.clients) ||
|
|
250
|
+
!o.clients.length ||
|
|
251
|
+
o.clients.length > 16) {
|
|
126
252
|
return invalid("oidc mode needs oidc.issuer_port and clients.");
|
|
127
253
|
}
|
|
128
254
|
const clients = o.clients.map(readClient);
|
|
129
|
-
if (clients.some(c => !c) ||
|
|
255
|
+
if (clients.some((c) => !c) ||
|
|
256
|
+
new Set(clients.map((c) => c.id)).size !== clients.length)
|
|
130
257
|
return invalid("each oidc client needs a unique id and redirect_paths (local paths) or native_redirects (custom schemes).");
|
|
131
258
|
const rewrite = o.rewrite_json ?? [];
|
|
132
|
-
if (!Array.isArray(rewrite) ||
|
|
259
|
+
if (!Array.isArray(rewrite) ||
|
|
260
|
+
!rewrite.every((r) => record(r) &&
|
|
261
|
+
only(r, ["method", "path", "field"]) &&
|
|
262
|
+
/^[A-Z]{3,7}$/.test(String(r.method)) &&
|
|
263
|
+
localPath(r.path) &&
|
|
264
|
+
name(r.field))) {
|
|
133
265
|
return invalid("oidc.rewrite_json entries are { method, path, field }.");
|
|
134
266
|
}
|
|
135
|
-
gateway.oidc = {
|
|
267
|
+
gateway.oidc = {
|
|
268
|
+
issuer_port: o.issuer_port,
|
|
269
|
+
clients: clients,
|
|
270
|
+
rewrite_json: rewrite,
|
|
271
|
+
};
|
|
136
272
|
}
|
|
137
273
|
if (v.mode === "token") {
|
|
138
274
|
const t = v.token;
|
|
139
|
-
const args = record(t) ? t.args ?? [] : [];
|
|
140
|
-
if (!record(t) ||
|
|
275
|
+
const args = record(t) ? (t.args ?? []) : [];
|
|
276
|
+
if (!record(t) ||
|
|
277
|
+
!only(t, ["exec", "args"]) ||
|
|
278
|
+
!relativeExec(t.exec) ||
|
|
279
|
+
!strings(args, token))
|
|
141
280
|
return invalid("token mode needs token.exec, a program in the daemon payload.");
|
|
142
|
-
if (!Object.
|
|
281
|
+
if (!Object.keys(daemon.targets).length ||
|
|
282
|
+
!Object.values(daemon.targets).every((dir) => existsSync(join(dir, t.exec))))
|
|
143
283
|
return invalid(`token.exec ${t.exec} is not in the daemon payload.`);
|
|
144
284
|
gateway.token = { exec: t.exec, args: args };
|
|
145
285
|
}
|
|
@@ -149,13 +289,16 @@ export function readIdentityGateway(value, daemon, fail) {
|
|
|
149
289
|
return invalid(account);
|
|
150
290
|
gateway.account = account;
|
|
151
291
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
292
|
+
if (!packagedPayload &&
|
|
293
|
+
Object.values(daemon.targets).some((dir) => {
|
|
294
|
+
try {
|
|
295
|
+
lstatSync(join(dir, GATEWAY_PAYLOAD_DIR));
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
})) {
|
|
159
302
|
return invalid(`the daemon payload must not contain ${GATEWAY_PAYLOAD_DIR}/, where the gateway is installed.`);
|
|
160
303
|
}
|
|
161
304
|
return gateway;
|
|
@@ -164,26 +307,70 @@ const packageDest = (m) => `/var/packages/${m.package}/target`;
|
|
|
164
307
|
/** target/sso/gateway.json, the SDK gateway's configuration (idgateway.Config). */
|
|
165
308
|
export function identityGatewayConfig(m) {
|
|
166
309
|
const g = m.identityGateway;
|
|
167
|
-
return JSON.stringify({
|
|
168
|
-
package: m.package,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
...(g.
|
|
172
|
-
...(
|
|
310
|
+
return (JSON.stringify({
|
|
311
|
+
package: m.package,
|
|
312
|
+
mode: g.mode,
|
|
313
|
+
access: g.access ?? "dsm-users",
|
|
314
|
+
...(g.groups ? { groups: g.groups } : {}),
|
|
315
|
+
...(m.entry ? { entry: m.entry } : {}),
|
|
316
|
+
...(m.compose?.plan.mounts_blocked ? { compose_blocked: true } : {}),
|
|
317
|
+
...(g.max_upload_bytes ? { max_upload_bytes: g.max_upload_bytes } : {}),
|
|
318
|
+
...(g.response_header_timeout_seconds
|
|
319
|
+
? { response_header_timeout_seconds: g.response_header_timeout_seconds }
|
|
320
|
+
: {}),
|
|
321
|
+
upstream: m.compose?.plan.ingress
|
|
322
|
+
? `http://${m.compose.plan.ingress.address}:${m.compose.plan.ingress.port}`
|
|
323
|
+
: `http://127.0.0.1:${g.upstream}`,
|
|
324
|
+
...(m.compose?.plan.ingress
|
|
325
|
+
? { compose_ingress: { candidate: m.compose.plan.ingress.candidate } }
|
|
326
|
+
: {}),
|
|
327
|
+
listen: g.listen,
|
|
328
|
+
authorize_cgi: `/webman/3rdparty/${m.package}/sso/authorize.cgi`,
|
|
329
|
+
state_dir: `/var/packages/${m.package}/var/sso`,
|
|
330
|
+
passthrough: g.passthrough,
|
|
331
|
+
redirects: g.redirects,
|
|
332
|
+
...(g.email_domain ? { email_domain: g.email_domain } : {}),
|
|
333
|
+
...(g.session_ttl ? { session_ttl_seconds: g.session_ttl } : {}),
|
|
334
|
+
...(g.roles ? { roles: g.roles } : {}),
|
|
335
|
+
...(g.headers ? { headers: g.headers } : {}),
|
|
173
336
|
...(g.oidc ? { oidc: g.oidc } : {}),
|
|
174
|
-
...(g.token
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
337
|
+
...(g.token
|
|
338
|
+
? {
|
|
339
|
+
token: {
|
|
340
|
+
command: [
|
|
341
|
+
`${packageDest(m)}/daemon/payload/${g.token.exec}`,
|
|
342
|
+
...g.token.args,
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
: {}),
|
|
347
|
+
...(g.account
|
|
348
|
+
? {
|
|
349
|
+
account: {
|
|
350
|
+
var_dir: `/var/packages/${m.package}/var`,
|
|
351
|
+
on_delete: g.account.on_delete.map((a) => "remove" in a
|
|
352
|
+
? a
|
|
353
|
+
: {
|
|
354
|
+
exec: [
|
|
355
|
+
`${packageDest(m)}/daemon/payload/${a.exec[0]}`,
|
|
356
|
+
...a.exec.slice(1),
|
|
357
|
+
],
|
|
358
|
+
}),
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
: {}),
|
|
362
|
+
}) + "\n");
|
|
180
363
|
}
|
|
181
364
|
/** The daemon as the controller runs it: the gateway, with the App as its child. */
|
|
182
365
|
export function gatewayDaemon(daemon) {
|
|
183
366
|
const app = `\${SYNOPKG_PKGDEST}/daemon/payload/${daemon.exec}`;
|
|
184
367
|
return {
|
|
185
|
-
...daemon,
|
|
186
|
-
|
|
368
|
+
...daemon,
|
|
369
|
+
exec: GATEWAY_EXEC,
|
|
370
|
+
args: ["run", "--", app, ...daemon.args],
|
|
371
|
+
...(daemon.uninstall_args
|
|
372
|
+
? { uninstall_args: ["exec", "--", app, ...daemon.uninstall_args] }
|
|
373
|
+
: {}),
|
|
187
374
|
};
|
|
188
375
|
}
|
|
189
376
|
/** Builds the gateway from the declared SDK release with the pinned Go, binding its config path. */
|
|
@@ -199,8 +386,18 @@ export async function buildIdentityGateway(m, arch, work, run) {
|
|
|
199
386
|
}
|
|
200
387
|
const out = join(work, `identity-gateway-${arch}`);
|
|
201
388
|
const ldflags = `-s -w -buildid= -X main.configPath=${packageDest(m)}/${GATEWAY_CONFIG_PATH}`;
|
|
202
|
-
const result = await run("go", [
|
|
203
|
-
|
|
389
|
+
const result = await run("go", [
|
|
390
|
+
"build",
|
|
391
|
+
"-trimpath",
|
|
392
|
+
"-buildvcs=false",
|
|
393
|
+
"-ldflags",
|
|
394
|
+
ldflags,
|
|
395
|
+
"-o",
|
|
396
|
+
out,
|
|
397
|
+
GATEWAY_MAIN,
|
|
398
|
+
], {
|
|
399
|
+
cwd: sdk.goDir,
|
|
400
|
+
env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
|
|
204
401
|
});
|
|
205
402
|
if (result.missing || result.status !== 0) {
|
|
206
403
|
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}`);
|
|
@@ -213,3 +410,22 @@ export async function buildIdentityGateway(m, arch, work, run) {
|
|
|
213
410
|
throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_BUILD_FAILED", toolchain);
|
|
214
411
|
return out;
|
|
215
412
|
}
|
|
413
|
+
/** The manifest form of a normalized gateway, retained for artifact inspection. */
|
|
414
|
+
export function identityGatewayDeclaration(g) {
|
|
415
|
+
if (!g)
|
|
416
|
+
return undefined;
|
|
417
|
+
return {
|
|
418
|
+
...g,
|
|
419
|
+
...(g.oidc
|
|
420
|
+
? {
|
|
421
|
+
oidc: {
|
|
422
|
+
...g.oidc,
|
|
423
|
+
clients: g.oidc.clients.map(({ require_pkce, ...c }) => ({
|
|
424
|
+
...c,
|
|
425
|
+
pkce: require_pkce,
|
|
426
|
+
})),
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
: {}),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type WebLaunchMode } from "../contract/web-launch.js";
|
|
2
|
+
import { type ComposeApp } from "./compose.js";
|
|
2
3
|
import { type ExecutorInspection } from "../runtime/executor-inspect.js";
|
|
3
4
|
import { type AppScriptName } from "./app-scripts.js";
|
|
4
5
|
import type { EntryMode } from "./manifest.js";
|
|
@@ -23,6 +24,17 @@ export interface SpkInspection {
|
|
|
23
24
|
name: string;
|
|
24
25
|
requiresAdminApproval: boolean;
|
|
25
26
|
}>;
|
|
27
|
+
compose?: {
|
|
28
|
+
ingress?: ComposeApp["plan"]["ingress"];
|
|
29
|
+
images: unknown[];
|
|
30
|
+
services: unknown[];
|
|
31
|
+
mountsBlocked: boolean;
|
|
32
|
+
acceptanceBlockers: string[];
|
|
33
|
+
readiness: {
|
|
34
|
+
profile: string;
|
|
35
|
+
limitation: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
26
38
|
executor?: ExecutorInspection;
|
|
27
39
|
uninstallCleanup?: UninstallCleanupItem[];
|
|
28
40
|
/** Application Portal entry; inactive until an administrator applies the alias. */
|