@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +33 -18
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -846
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/unsupported-types.js +1 -1
  14. package/dist/contract/validate.js +2 -2
  15. package/dist/contract/web-launch.d.ts +1 -12
  16. package/dist/contract/web-launch.js +10 -74
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/e2e/dsm.d.ts +2 -2
  46. package/dist/e2e/dsm.js +10 -6
  47. package/dist/error-registry.js +1020 -834
  48. package/dist/host/share-permission-lookup.d.ts +1 -1
  49. package/dist/host/share-permission.d.ts +1 -1
  50. package/dist/host/share-permission.js +1 -1
  51. package/dist/info.js +120 -28
  52. package/dist/init.js +188 -64
  53. package/dist/inspect.js +21 -6
  54. package/dist/runtime/executor-digests.json +5 -5
  55. package/dist/skill.js +51 -9
  56. package/dist/standalone/api-description-inspect.d.ts +4 -0
  57. package/dist/standalone/api-description-inspect.js +12 -0
  58. package/dist/standalone/api-description.d.ts +7 -0
  59. package/dist/standalone/api-description.js +73 -0
  60. package/dist/standalone/build.d.ts +3 -0
  61. package/dist/standalone/build.js +140 -33
  62. package/dist/standalone/compose-errors.d.ts +3 -0
  63. package/dist/standalone/compose-errors.js +243 -0
  64. package/dist/standalone/compose.d.ts +97 -10
  65. package/dist/standalone/compose.js +306 -75
  66. package/dist/standalone/daemon.js +111 -29
  67. package/dist/standalone/dsm-window.js +39 -8
  68. package/dist/standalone/identity-gateway.d.ts +9 -2
  69. package/dist/standalone/identity-gateway.js +282 -66
  70. package/dist/standalone/inspect-spk.d.ts +12 -0
  71. package/dist/standalone/inspect-spk.js +666 -136
  72. package/dist/standalone/lifecycle.js +124 -42
  73. package/dist/standalone/manifest.d.ts +8 -1
  74. package/dist/standalone/manifest.js +303 -86
  75. package/dist/standalone/resource.js +6 -2
  76. package/dist/standalone/spk.d.ts +4 -1
  77. package/dist/standalone/spk.js +138 -39
  78. package/dist/template-paths.d.ts +0 -2
  79. package/dist/template-paths.js +0 -5
  80. package/dist/test-command.d.ts +1 -1
  81. package/dist/test-command.js +1 -1
  82. package/dist/validate.d.ts +3 -3
  83. package/dist/validate.js +32 -140
  84. package/package.json +5 -3
  85. package/schema/synopkgland-api-v1.schema.json +423 -0
  86. package/skills/SKILL.md +4 -2
  87. package/skills/reference/compose.md +349 -0
  88. package/skills/reference/create.md +9 -18
  89. package/skills/reference/ctl.md +41 -0
  90. package/skills/reference/deploy.md +1 -1
  91. package/skills/reference/files.md +5 -1
  92. package/skills/reference/identity-gateway.md +8 -144
  93. package/skills/reference/wrap.md +10 -39
  94. package/assets/go/daemon/fixture_linux_test.go +0 -15
  95. package/assets/go/daemon/fixture_other_test.go +0 -7
  96. package/assets/go/daemon/go.mod +0 -5
  97. package/assets/go/daemon/main.go +0 -636
  98. package/assets/go/daemon/main_test.go +0 -315
  99. package/assets/go/daemon/network.go +0 -107
  100. package/assets/go/daemon/network_test.go +0 -86
  101. package/assets/go/daemon/process_linux.go +0 -55
  102. package/assets/go/daemon/process_linux_test.go +0 -161
  103. package/assets/go/daemon/process_other.go +0 -14
  104. package/assets/go/daemon/testdata/child.go +0 -44
  105. package/assets/go/gateway/auth.go +0 -101
  106. package/assets/go/gateway/auth_test.go +0 -216
  107. package/assets/go/gateway/cgi.go +0 -133
  108. package/assets/go/gateway/cgi_test.go +0 -156
  109. package/assets/go/gateway/csrf.go +0 -73
  110. package/assets/go/gateway/csrf_test.go +0 -155
  111. package/assets/go/gateway/directory.go +0 -108
  112. package/assets/go/gateway/directory_test.go +0 -202
  113. package/assets/go/gateway/gateway.go +0 -412
  114. package/assets/go/gateway/gateway_test.go +0 -676
  115. package/assets/go/gateway/go.mod +0 -5
  116. package/assets/go/gateway/groups.go +0 -66
  117. package/assets/go/gateway/main.go +0 -45
  118. package/dist/build-spk/gateway.d.ts +0 -31
  119. package/dist/build-spk/gateway.js +0 -60
  120. package/dist/compose/privilege.d.ts +0 -14
  121. 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" && v.length <= 512 && /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/.test(v)
22
- && !v.startsWith("//") && !v.split("/").some(p => p === "." || p === "..");
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" && v.length <= 512 && /^[a-z][a-z0-9+.-]*:[^\s]*$/.test(v)
29
- && !/^(https?|javascript|data|vbscript|file|blob|about):/.test(v);
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" && 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);
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) || !only(v, ["on_delete"]) || !Array.isArray(v.on_delete) || v.on_delete.length > 16)
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) || !only(a, ["exec"]) || !Array.isArray(a.exec) || !relativeExec(a.exec[0]) || !strings(a.exec.slice(1), s => token(s) && placeholders(s))) {
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.values(daemon.targets).every(dir => existsSync(join(dir, argv[0]))))
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 = { 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;
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) || !only(v, ["id", "redirect_paths", "native_redirects", "pkce", "roles"]))
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) || !strings(v.redirect_paths, localPath) || !strings(native, nativeRedirect) || (v.pkce !== undefined && typeof v.pkce !== "boolean") || roles === false)
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 { id: v.id, redirect_paths: v.redirect_paths, native_redirects: native, require_pkce: v.pkce === true, ...(roles ? { roles } : {}) };
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) => { fail({ code: "STANDALONE_IDENTITY_GATEWAY_INVALID", message: `identity_gateway: ${why}` }); return undefined; };
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) || !only(value, ["sdk", "mode", "upstream", "listen", "passthrough", "redirects", "email_domain", "session_ttl", "roles", "headers", "oidc", "token", "account"])) {
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 !== "oidc" && v.mode !== "header" && v.mode !== "token")
87
- return invalid("mode must be oidc, header or token.");
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) || !only(listen, ["http", "https"]) || (listen.http === undefined && listen.https === undefined)
92
- || ![listen.http, listen.https].every(p => p === undefined || port(p)))
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 = [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)))
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) || Object.entries(redirects).some(([from, to]) => !localPath(from) || !localPath(to)))
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 && (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)))
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 && (!Number.isInteger(v.session_ttl) || v.session_ttl < 60 || v.session_ttl > 86400))
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 && (!record(headers) || !only(headers, ["user", "email", "name", "groups", "uid"]) || !Object.values(headers).every(headerName))) {
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, 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 } : {}),
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) || !only(o, ["issuer_port", "clients", "rewrite_json"]) || !port(o.issuer_port) || !Array.isArray(o.clients) || !o.clients.length || o.clients.length > 16) {
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) || new Set(clients.map(c => c.id)).size !== clients.length)
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) || !rewrite.every(r => record(r) && only(r, ["method", "path", "field"]) && /^[A-Z]{3,7}$/.test(String(r.method)) && localPath(r.path) && name(r.field))) {
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 = { issuer_port: o.issuer_port, clients: clients, rewrite_json: rewrite };
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) || !only(t, ["exec", "args"]) || !relativeExec(t.exec) || !strings(args, token))
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.values(daemon.targets).every(dir => existsSync(join(dir, t.exec))))
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 (Object.values(daemon.targets).some(dir => { try {
153
- lstatSync(join(dir, GATEWAY_PAYLOAD_DIR));
154
- return true;
155
- }
156
- catch {
157
- return false;
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, 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 } : {}),
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 ? { 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";
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, exec: GATEWAY_EXEC, args: ["run", "--", app, ...daemon.args],
186
- ...(daemon.uninstall_args ? { uninstall_args: ["exec", "--", app, ...daemon.uninstall_args] } : {}),
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", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, GATEWAY_MAIN], {
203
- cwd: sdk.goDir, env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
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. */