@catheadowl/ukp 0.2.2 → 0.2.4
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/README.md +50 -40
- package/bin/ukp.js +31 -11
- package/docs/remote-deployment.md +309 -0
- package/package.json +2 -1
- package/src/capabilities/propose.ts +1 -1
- package/src/capabilities/read.ts +1 -1
- package/src/capabilities/remote-client.ts +163 -52
- package/src/capabilities/rg.ts +1 -1
- package/src/capabilities/search.ts +2 -2
- package/src/cli.ts +74 -5
- package/src/commands/diagnose.ts +1 -1
- package/src/commands/guide-content/propose.txt +1 -1
- package/src/commands/guide-content/remote.txt +6 -5
- package/src/commands/guide-content/service-qmd.txt +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/inspect.ts +1 -1
- package/src/commands/inventory.ts +194 -36
- package/src/commands/propose.ts +1 -1
- package/src/commands/read.ts +7 -7
- package/src/commands/serve-task.ts +198 -0
- package/src/commands/serve.ts +67 -22
- package/src/registry.ts +2 -2
- package/src/server.ts +4 -4
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** Windows resident-door artifact generator (W12): `ukp serve --print-task`
|
|
2
|
+
* renders the process-manager artifacts for an https door on a Windows host
|
|
3
|
+
* — the start script, the hidden launcher (vbs), the Task Scheduler command,
|
|
4
|
+
* the firewall rule — as text the operator reviews and applies. Print-only
|
|
5
|
+
* by construction: UKP installs nothing, starts nothing, and never sees the
|
|
6
|
+
* real token (the script line carries a placeholder; the operator pastes the
|
|
7
|
+
* token into the file they save, under their own permission regime).
|
|
8
|
+
*
|
|
9
|
+
* The emitted shapes mirror the machine-verified liku recipe (deployment
|
|
10
|
+
* handbook, Windows section): a bare `ukp` resolves through the logged-on
|
|
11
|
+
* task's per-user PATH and the self-locating launcher finds bun itself, so
|
|
12
|
+
* no PATH export is emitted. The hidden launcher is the product default
|
|
13
|
+
* (owner ruling 2026-09-21: a visibly popping cmd window is not a correct
|
|
14
|
+
* product form): zero flash (hidden at process creation), door.log capture,
|
|
15
|
+
* exit-code propagation, bounded crash retry — Task Scheduler's own
|
|
16
|
+
* restart-on-failure does not fire on exit codes (disproven on liku). */
|
|
17
|
+
|
|
18
|
+
export interface ServeTaskTlsSelfSigned {
|
|
19
|
+
mode: "self-signed";
|
|
20
|
+
sanEntries: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ServeTaskTlsCertificates {
|
|
24
|
+
mode: "certificates";
|
|
25
|
+
certPath: string;
|
|
26
|
+
keyPath: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ServeTaskTls = ServeTaskTlsSelfSigned | ServeTaskTlsCertificates;
|
|
30
|
+
|
|
31
|
+
export interface ServeTaskInput {
|
|
32
|
+
/** Present = single-endpoint mode; absent = host door mode. */
|
|
33
|
+
endpoint?: string;
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
tls?: ServeTaskTls;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Door-shape guards the command layer runs before printing. Pure so both
|
|
40
|
+
* platforms are testable: `platform` is `process.platform` in production.
|
|
41
|
+
* These mirror what serve itself would refuse at task start — they never
|
|
42
|
+
* widen admission, and the TLS one stops a dead-end artifact before it is
|
|
43
|
+
* applied (clients refuse to register plain-http urls off loopback). */
|
|
44
|
+
export function printTaskPreflightError(input: {
|
|
45
|
+
platform: string;
|
|
46
|
+
host: string;
|
|
47
|
+
hasTls: boolean;
|
|
48
|
+
allowAnonymous: boolean;
|
|
49
|
+
}): string | undefined {
|
|
50
|
+
if (input.platform !== "win32") {
|
|
51
|
+
return "--print-task is Windows-only; on Linux prefer systemd socket activation (ukp serve --systemd-socket) - the deployment handbook carries that recipe";
|
|
52
|
+
}
|
|
53
|
+
const loopback =
|
|
54
|
+
input.host === "127.0.0.1" || input.host === "localhost" || input.host === "::1" || input.host === "[::1]";
|
|
55
|
+
if (!loopback && !input.hasTls) {
|
|
56
|
+
return "refusing to print an off-loopback door without TLS: clients refuse to register plain-http urls - pass --tls (self-signed, pinned at registration) or --tls-cert/--tls-key, or bind loopback behind a TLS-terminating proxy";
|
|
57
|
+
}
|
|
58
|
+
if (input.allowAnonymous && !loopback) {
|
|
59
|
+
return "refusing to print this shape: --allow-anonymous is loopback-only and serve would refuse it at task start - set the token in the generated script instead";
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function serveCommandLine(input: ServeTaskInput): string {
|
|
65
|
+
const tls =
|
|
66
|
+
input.tls === undefined
|
|
67
|
+
? []
|
|
68
|
+
: input.tls.mode === "self-signed"
|
|
69
|
+
? ["--tls", ...input.tls.sanEntries.map((entry) => `--tls-san ${entry}`)]
|
|
70
|
+
: [`--tls-cert "${input.tls.certPath}"`, `--tls-key "${input.tls.keyPath}"`];
|
|
71
|
+
return [
|
|
72
|
+
"ukp serve",
|
|
73
|
+
...(input.endpoint !== undefined ? [`--endpoint ${input.endpoint}`] : []),
|
|
74
|
+
`--host ${input.host}`,
|
|
75
|
+
`--port ${input.port}`,
|
|
76
|
+
...tls,
|
|
77
|
+
].join(" ");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Parent directory of a path string, or undefined for a bare filename. */
|
|
81
|
+
function parentDirOf(path: string): string | undefined {
|
|
82
|
+
const cut = Math.max(path.lastIndexOf("\\"), path.lastIndexOf("/"));
|
|
83
|
+
return cut > 0 ? path.slice(0, cut) : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function renderServeTaskArtifacts(input: ServeTaskInput): string {
|
|
87
|
+
const serveLine = serveCommandLine(input);
|
|
88
|
+
const out: string[] = [
|
|
89
|
+
"ukp serve --print-task - Windows resident-door artifacts",
|
|
90
|
+
"",
|
|
91
|
+
"Print-only: review these, then apply them yourself. UKP installs nothing,",
|
|
92
|
+
"starts nothing, and supervises nothing - the door's lifecycle belongs to",
|
|
93
|
+
"you and your process manager.",
|
|
94
|
+
"",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
if (input.tls?.mode === "certificates") {
|
|
98
|
+
const certDir = parentDirOf(input.tls.certPath);
|
|
99
|
+
out.push(
|
|
100
|
+
"0) Certificate - generate ONCE (Git for Windows' openssl works; this",
|
|
101
|
+
" avoids any runtime openssl dependency of --tls):",
|
|
102
|
+
"",
|
|
103
|
+
" openssl req -x509 -newkey rsa:2048 -nodes -days 825 -keyout",
|
|
104
|
+
` ${input.tls.keyPath} -out ${input.tls.certPath}`,
|
|
105
|
+
" -subj \"/CN=ukp\" -addext \"subjectAltName=IP:<lan-ip>,DNS:localhost\"",
|
|
106
|
+
"",
|
|
107
|
+
" The SAN must carry the address consumers dial (find <lan-ip> with",
|
|
108
|
+
" ipconfig); a mismatch fails registration with a certificate name",
|
|
109
|
+
" error. Renewing over the same key keeps client pins valid.",
|
|
110
|
+
"",
|
|
111
|
+
);
|
|
112
|
+
if (certDir !== undefined) {
|
|
113
|
+
out.push(` (openssl will not create the directory: if not exist "${certDir}" mkdir "${certDir}" first)`, "");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
out.push(
|
|
118
|
+
"1) Start script - save as %USERPROFILE%\\.ukp\\start-door.cmd",
|
|
119
|
+
" (write it with a real editor and CRLF line endings; writing cmd",
|
|
120
|
+
" files over ssh echo mangles % escaping)",
|
|
121
|
+
"",
|
|
122
|
+
" @echo off",
|
|
123
|
+
" rem UKP resident door (generated by ukp serve --print-task)",
|
|
124
|
+
" rem The token line is the only secret: mint a long random string",
|
|
125
|
+
" rem (e.g. openssl rand -hex 32), paste it in, keep this file private.",
|
|
126
|
+
" set \"UKP_SERVE_TOKEN=<paste-your-token>\"",
|
|
127
|
+
" rem ukp resolves through the logged-on task's per-user PATH; the",
|
|
128
|
+
" rem launcher finds bun itself - no PATH export needed.",
|
|
129
|
+
" if not exist \"%USERPROFILE%\\.ukp\" mkdir \"%USERPROFILE%\\.ukp\"",
|
|
130
|
+
` ${serveLine} >> "%USERPROFILE%\\.ukp\\door.log" 2>&1`,
|
|
131
|
+
"",
|
|
132
|
+
"2) Hidden launcher - save as %USERPROFILE%\\.ukp\\start-door-hidden.vbs",
|
|
133
|
+
" (CRLF and a real editor again: the vbs carries literal % signs that",
|
|
134
|
+
" ssh echo would expand - copy the file instead)",
|
|
135
|
+
"",
|
|
136
|
+
" ' UKP door hidden launcher: runs start-door.cmd with NO visible",
|
|
137
|
+
" ' window (hidden at process creation - nothing flashes), captures",
|
|
138
|
+
" ' everything to door.log, waits out the door's lifetime, propagates",
|
|
139
|
+
" ' its exit code, and retries a crashed door 3 times, 30s apart.",
|
|
140
|
+
" Dim shell : Set shell = CreateObject(\"WScript.Shell\")",
|
|
141
|
+
" Dim q : q = Chr(34)",
|
|
142
|
+
" Dim home : home = shell.ExpandEnvironmentStrings(\"%USERPROFILE%\")",
|
|
143
|
+
" Dim cmdline : cmdline = \"cmd /c \" & q & q & home & \"\\.ukp\\start-door.cmd\" & q & \" >> \" & q & home & \"\\.ukp\\door.log\" & q & \" 2>&1\" & q",
|
|
144
|
+
" Dim rc : rc = 0",
|
|
145
|
+
" Dim attempt",
|
|
146
|
+
" For attempt = 1 To 3",
|
|
147
|
+
" rc = shell.Run(cmdline, 0, True)",
|
|
148
|
+
" If rc = 0 Then Exit For",
|
|
149
|
+
" If attempt < 3 Then WScript.Sleep 30000",
|
|
150
|
+
" Next",
|
|
151
|
+
" WScript.Quit rc",
|
|
152
|
+
"",
|
|
153
|
+
"3) Scheduled task - starts the door at LOGON, hidden",
|
|
154
|
+
" (create it from an elevated shell: creating an ONLOGON task from a",
|
|
155
|
+
" plain shell fails with Access denied)",
|
|
156
|
+
"",
|
|
157
|
+
" schtasks /Create /TN ukp-door /TR \"wscript.exe \\\"%USERPROFILE%\\.ukp\\start-door-hidden.vbs\\\"\" /SC ONLOGON /F",
|
|
158
|
+
" schtasks /Run /TN ukp-door",
|
|
159
|
+
"",
|
|
160
|
+
"4) Firewall rule for the port (from an elevated shell)",
|
|
161
|
+
"",
|
|
162
|
+
` netsh advfirewall firewall add rule name="ukp-door" dir=in action=allow protocol=TCP localport=${input.port}`,
|
|
163
|
+
"",
|
|
164
|
+
"Notes:",
|
|
165
|
+
...(input.tls?.mode === "self-signed"
|
|
166
|
+
? [
|
|
167
|
+
"- --tls self-signs at first start using an openssl on PATH (Git",
|
|
168
|
+
" for Windows' openssl qualifies). On a host with no openssl",
|
|
169
|
+
" anywhere, pre-generate the certificate once instead: re-run with",
|
|
170
|
+
" --tls-cert/--tls-key - the handbook's Windows section carries",
|
|
171
|
+
" that recipe.",
|
|
172
|
+
]
|
|
173
|
+
: []),
|
|
174
|
+
"- No visible window, by design: the hidden launcher starts the console",
|
|
175
|
+
" hidden at process creation (nothing ever flashes on the desktop) and",
|
|
176
|
+
" captures everything to door.log - that redirect is the paper trail (a",
|
|
177
|
+
" silent EADDRINUSE exit leaves a line).",
|
|
178
|
+
"- Crash self-recovery is the launcher's bounded retry: a non-zero exit",
|
|
179
|
+
" is retried up to 3 times, 30s apart; exhausted retries leave the",
|
|
180
|
+
" failure visible (non-zero task result + door.log). Do not rely on",
|
|
181
|
+
" Task Scheduler's restart-on-failure setting - it does not fire on",
|
|
182
|
+
" exit codes.",
|
|
183
|
+
"- To restart the door by hand (new token, ukp upgrade, new cert):",
|
|
184
|
+
" taskkill /PID <door-pid> /T /F, then schtasks /Run /TN ukp-door",
|
|
185
|
+
" (schtasks /End does not reliably kill the process tree).",
|
|
186
|
+
"- LOGON, not boot: the door starts when someone logs on - nobody",
|
|
187
|
+
" logged on means no door. Unattended-boot always-on needs a service",
|
|
188
|
+
" wrapper (WinSW-class) - deliberately outside this recipe; prefer the",
|
|
189
|
+
" ssh:// path or a Linux host (systemd socket activation) for that.",
|
|
190
|
+
"- Re-run ukp serve --print-task with different flags to regenerate;",
|
|
191
|
+
" edits land by saving the files and re-running schtasks /Run.",
|
|
192
|
+
"- Full recipes and hardening: the deployment handbook -",
|
|
193
|
+
" docs/remote-deployment.md in this package, or",
|
|
194
|
+
" https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md",
|
|
195
|
+
"",
|
|
196
|
+
);
|
|
197
|
+
return out.join("\n");
|
|
198
|
+
}
|
package/src/commands/serve.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./kit.ts";
|
|
10
10
|
import { normalizeTlsSanEntry } from "../capabilities/tls-identity.ts";
|
|
11
11
|
import { DISCOVERY_PATH, startUkpServer, type ServeInfo } from "../server.ts";
|
|
12
|
+
import { printTaskPreflightError, renderServeTaskArtifacts, type ServeTaskTls } from "./serve-task.ts";
|
|
12
13
|
|
|
13
14
|
export interface ServeCommandContext {
|
|
14
15
|
currentDirectory: string;
|
|
@@ -33,22 +34,23 @@ function parseServeTokens(raw: string | undefined): string[] {
|
|
|
33
34
|
* over `/e/<name>/` routing. */
|
|
34
35
|
export const SERVE_SPEC: UkpCommandSpec = {
|
|
35
36
|
name: "serve",
|
|
36
|
-
summary: "serve one endpoint
|
|
37
|
+
summary: "serve one endpoint - or the whole registry as a host door, one HTTP server for every endpoint - for remote UKP clients",
|
|
37
38
|
group: "operations",
|
|
38
39
|
description:
|
|
39
|
-
"Expose a registered endpoint over HTTP using the ukp-remote wire, or
|
|
40
|
-
usage: "[--endpoint <name>] [--host <addr>] [--port <n>] [--tls [--tls-san <ip|dns>]
|
|
40
|
+
"Expose a registered endpoint over HTTP using the ukp-remote wire, or - without --endpoint - serve every local endpoint as one host door routed by name.",
|
|
41
|
+
usage: "[--endpoint <name>] [--host <addr>] [--port <n>] [--tls [--tls-san <ip|dns>]... | --tls-cert <pem> --tls-key <pem>] [--max-idle <seconds>] [--systemd-socket] [--print-task]",
|
|
41
42
|
options: [
|
|
42
43
|
{ flags: "--endpoint <name>", help: "the registered endpoint to expose; omit it to serve the whole registry as a host door (/e/<name>/ routing, all endpoints, one port)" },
|
|
43
44
|
{ flags: "--host <addr>", help: "listen address (default 127.0.0.1, loopback only without a token)" },
|
|
44
45
|
{ flags: "--port <n>", help: "listen port (default 8570)" },
|
|
45
46
|
{ flags: "--tls", help: "serve HTTPS with a self-signed identity (auto-generated under .ukp/tls/, SAN covers this host's addresses; clients pin it at registration)" },
|
|
46
47
|
{ flags: "--tls-san <ip|dns>", help: "extra SAN entry for the --tls self-signed identity (repeatable): a public/NAT IP or hostname no NIC of this host carries; a persisted certificate missing an entry is re-signed over the same key (pin unchanged, pinned clients re-anchor)", multi: true },
|
|
47
|
-
{ flags: "--tls-cert <pem>", help: "TLS certificate (chain) PEM path
|
|
48
|
+
{ flags: "--tls-cert <pem>", help: "TLS certificate (chain) PEM path - Let's Encrypt, mkcert, or a private CA; pair with --tls-key" },
|
|
48
49
|
{ flags: "--tls-key <pem>", help: "TLS private key PEM path; pair with --tls-cert" },
|
|
49
50
|
{ flags: "--allow-anonymous", help: "permit tokenless access on loopback (local testing, or an ssh-forwarded host door where SSH carries encryption and auth; reverse-proxy deployments still require UKP_SERVE_TOKEN)" },
|
|
50
|
-
{ flags: "--max-idle <seconds>", help: "exit after <seconds> without requests (self-reap; the orphan backstop for on-demand-woken doors
|
|
51
|
-
{ flags: "--systemd-socket", help: "serve on the systemd socket-activation listener (LISTEN_FDS fd 3) instead of binding a port
|
|
51
|
+
{ flags: "--max-idle <seconds>", help: "exit after <seconds> without requests (self-reap; the orphan backstop for on-demand-woken doors - fractional values accepted for tests)" },
|
|
52
|
+
{ flags: "--systemd-socket", help: "serve on the systemd socket-activation listener (LISTEN_FDS fd 3) instead of binding a port - the port belongs to your .socket unit; Linux-only" },
|
|
53
|
+
{ flags: "--print-task", help: "Windows: print this door's process-manager artifacts (start script, hidden launcher, Task Scheduler command, firewall rule) instead of serving - review and apply them yourself; nothing is installed or started for you, and the token is yours to paste (never printed)" },
|
|
52
54
|
],
|
|
53
55
|
helpSuffix: [
|
|
54
56
|
"",
|
|
@@ -56,9 +58,9 @@ export const SERVE_SPEC: UkpCommandSpec = {
|
|
|
56
58
|
` GET ${DISCOVERY_PATH} discovery document (manifest projection,`,
|
|
57
59
|
" protocol version, instance identity)",
|
|
58
60
|
" POST /v1/search {query, limit} -> ukp.search.v1 envelope",
|
|
59
|
-
" GET /v1/read?ref
|
|
61
|
+
" GET /v1/read?ref=...|uri=... endpoint-relative ref or ukp:// URI",
|
|
60
62
|
" GET /v1/nav?path=&depth= markdown route view (ukp.nav.v1)",
|
|
61
|
-
" GET /v1/rg?query
|
|
63
|
+
" GET /v1/rg?query=... lexical search (ukp.rg.v1, ukp_uri handoff)",
|
|
62
64
|
"",
|
|
63
65
|
"Wire (ukp-remote v1), host door (no --endpoint):",
|
|
64
66
|
` GET ${DISCOVERY_PATH} door document (scope:"host", endpoint roster)`,
|
|
@@ -73,26 +75,26 @@ export const SERVE_SPEC: UkpCommandSpec = {
|
|
|
73
75
|
" --allow-anonymous, loopback binds only. The discovery document stays",
|
|
74
76
|
" public. A reverse proxy on the same host forwards from the public side",
|
|
75
77
|
" to the loopback bind, so proxy deployments treat the token as",
|
|
76
|
-
" mandatory
|
|
78
|
+
" mandatory - serve cannot see past its own bind address.",
|
|
77
79
|
"",
|
|
78
80
|
" Loopback-without-token covers testing/dogfood AND the ssh door",
|
|
79
81
|
" deployment: bind the door on the remote host's loopback and let ssh",
|
|
80
82
|
" forwarding carry encryption and authentication (clients register",
|
|
81
|
-
" 'ukp register --url ssh://<host>'
|
|
83
|
+
" 'ukp register --url ssh://<host>' - zero tokens, docker",
|
|
82
84
|
" DOCKER_HOST=ssh:// posture). It is not the way to consume a",
|
|
83
|
-
" same-machine endpoint
|
|
84
|
-
" TLS
|
|
85
|
-
" (generated under .ukp/tls/
|
|
86
|
-
" mode
|
|
85
|
+
" same-machine endpoint - register its local path instead.",
|
|
86
|
+
" TLS: pass --tls to serve HTTPS with a self-signed identity",
|
|
87
|
+
" (generated under .ukp/tls/ - under the registry directory in door",
|
|
88
|
+
" mode -, SAN covers this host's addresses; remote clients TOFU-pin it",
|
|
87
89
|
" at registration and refresh by re-registering), or --tls-cert/--tls-key",
|
|
88
|
-
" for your own certificate (Let's Encrypt
|
|
89
|
-
" 2026-01
|
|
90
|
+
" for your own certificate (Let's Encrypt - IP certs available since",
|
|
91
|
+
" 2026-01 -, mkcert, a private CA). Plain HTTP remains loopback-only by",
|
|
90
92
|
" admission; public exposure needs TLS or the ssh:// transport.",
|
|
91
93
|
"",
|
|
92
94
|
" A cloud NAT/EIP host's public IP is on no NIC (--tls alone cannot",
|
|
93
95
|
" cover it): add --tls-san <public-ip> next to --tls. Repeatable; DNS",
|
|
94
96
|
" names accepted. The persisted certificate re-signs over the same key",
|
|
95
|
-
" when a new entry appears
|
|
97
|
+
" when a new entry appears - the pin is unchanged and pinned clients",
|
|
96
98
|
" re-anchor transparently; dropping entries never re-signs.",
|
|
97
99
|
"",
|
|
98
100
|
].join("\n"),
|
|
@@ -107,6 +109,9 @@ export interface ParsedServe {
|
|
|
107
109
|
maxIdleSeconds?: number;
|
|
108
110
|
/** Present = the systemd-passed listener (W10, Linux-only). */
|
|
109
111
|
systemdSocket?: boolean;
|
|
112
|
+
/** Present = render the Windows process-manager artifacts instead of
|
|
113
|
+
* serving (W12; Windows-only, print-only, never starts a listener). */
|
|
114
|
+
printTask?: boolean;
|
|
110
115
|
/** `--tls-san` values, normalized to `IP:x`/`DNS:y` SAN form (repeatable);
|
|
111
116
|
* valid only next to `--tls` — the execute layer enforces the pairing.
|
|
112
117
|
* Absent when the flag is not passed (parse output stays byte-identical
|
|
@@ -115,7 +120,7 @@ export interface ParsedServe {
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
function toParsedServe(parsed: KitParsed): ParsedServe {
|
|
118
|
-
const options = parsed.options as { host?: string; port?: string; endpoint?: string; maxIdle?: string; systemdSocket?: boolean; tlsSan?: string[] };
|
|
123
|
+
const options = parsed.options as { host?: string; port?: string; endpoint?: string; maxIdle?: string; systemdSocket?: boolean; printTask?: boolean; tlsSan?: string[] };
|
|
119
124
|
const endpoint = options.endpoint;
|
|
120
125
|
if (endpoint !== undefined && endpoint.length === 0) {
|
|
121
126
|
throw new KitUsageError("--endpoint <name> must not be empty (omit it to serve a host door)");
|
|
@@ -129,12 +134,19 @@ function toParsedServe(parsed: KitParsed): ParsedServe {
|
|
|
129
134
|
if (systemdSocket && options.port !== undefined) {
|
|
130
135
|
throw new KitUsageError("--systemd-socket and --port are mutually exclusive (the socket unit owns the port)");
|
|
131
136
|
}
|
|
137
|
+
const printTask = options.printTask === true;
|
|
138
|
+
if (printTask && systemdSocket) {
|
|
139
|
+
throw new KitUsageError("--print-task is the Windows resident form; --systemd-socket is the Linux one (systemd holds the port - see the deployment handbook)");
|
|
140
|
+
}
|
|
132
141
|
let maxIdleSeconds: number | undefined;
|
|
133
142
|
if (options.maxIdle !== undefined) {
|
|
134
143
|
maxIdleSeconds = Number(options.maxIdle);
|
|
135
144
|
if (!Number.isFinite(maxIdleSeconds) || maxIdleSeconds <= 0 || maxIdleSeconds > 86400) {
|
|
136
145
|
throw new KitUsageError("--max-idle must be a positive number of seconds (at most 86400)");
|
|
137
146
|
}
|
|
147
|
+
if (printTask) {
|
|
148
|
+
throw new KitUsageError("--print-task is for a resident door; --max-idle self-reaps (it is the on-demand backstop) and would leave the door down until the next logon");
|
|
149
|
+
}
|
|
138
150
|
}
|
|
139
151
|
let tlsSan: string[] = [];
|
|
140
152
|
if (options.tlsSan !== undefined) {
|
|
@@ -152,6 +164,7 @@ function toParsedServe(parsed: KitParsed): ParsedServe {
|
|
|
152
164
|
port,
|
|
153
165
|
...(maxIdleSeconds !== undefined ? { maxIdleSeconds } : {}),
|
|
154
166
|
...(systemdSocket ? { systemdSocket } : {}),
|
|
167
|
+
...(printTask ? { printTask } : {}),
|
|
155
168
|
...(tlsSan.length > 0 ? { tlsSan } : {}),
|
|
156
169
|
};
|
|
157
170
|
}
|
|
@@ -170,10 +183,10 @@ export function renderServeBanner(info: ServeInfo): string {
|
|
|
170
183
|
`serving host door (ukp-remote v1)`,
|
|
171
184
|
` listening: ${info.url}`,
|
|
172
185
|
` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url} (host door)`,
|
|
173
|
-
` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none
|
|
186
|
+
` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none - register endpoints on this host)"}`,
|
|
174
187
|
` write: ${info.door!.write.length > 0 ? info.door!.write.join(", ") + " (propose via PUT /e/<name>/v1/propose/<id>)" : "(no endpoint declares propose)"}`,
|
|
175
188
|
` auth: ${info.authRequired ? "bearer token required" : "no token (loopback bind; ssh-forwarded clients authenticate by SSH key)"}`,
|
|
176
|
-
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning
|
|
189
|
+
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning - install ripgrep on this door's PATH)"}`,
|
|
177
190
|
...(info.maxIdleSeconds !== undefined
|
|
178
191
|
? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
|
|
179
192
|
: []),
|
|
@@ -188,7 +201,7 @@ export function renderServeBanner(info: ServeInfo): string {
|
|
|
188
201
|
` listening: ${info.url}`,
|
|
189
202
|
` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url}`,
|
|
190
203
|
` auth: ${info.authRequired ? "bearer token required" : "no token (loopback only)"}`,
|
|
191
|
-
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning
|
|
204
|
+
` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning - install ripgrep on this door's PATH)"}`,
|
|
192
205
|
...(info.maxIdleSeconds !== undefined
|
|
193
206
|
? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
|
|
194
207
|
: []),
|
|
@@ -230,6 +243,7 @@ export function executeServeCommand(
|
|
|
230
243
|
return executeKitCommand(SERVE_SPEC, args, (parsed) => {
|
|
231
244
|
const { endpoint, host, port, maxIdleSeconds, systemdSocket, tlsSan = [] } = toParsedServe(parsed);
|
|
232
245
|
const allowAnonymous = (parsed.options as { allowAnonymous?: boolean }).allowAnonymous === true;
|
|
246
|
+
const printTask = (parsed.options as { printTask?: boolean }).printTask === true;
|
|
233
247
|
const options = parsed.options as { tls?: boolean; tlsCert?: string; tlsKey?: string };
|
|
234
248
|
// TLS flag family (W5'): --tls and --tls-cert/--tls-key are mutually
|
|
235
249
|
// exclusive; the explicit pair must arrive complete. --tls-san only
|
|
@@ -244,13 +258,44 @@ export function executeServeCommand(
|
|
|
244
258
|
if (tlsSan.length > 0 && options.tls !== true) {
|
|
245
259
|
throw new KitUsageError("--tls-san is only used with --tls (an explicit --tls-cert certificate carries its own SAN)");
|
|
246
260
|
}
|
|
261
|
+
// W12 print-only branch: render the Windows artifacts and stop. No
|
|
262
|
+
// listener, no admission token check (the token is pasted into the
|
|
263
|
+
// generated script later), and the env token is deliberately not read
|
|
264
|
+
// — the output is secret-free by construction.
|
|
265
|
+
if (printTask) {
|
|
266
|
+
const preflight = printTaskPreflightError({
|
|
267
|
+
platform: process.platform,
|
|
268
|
+
host,
|
|
269
|
+
hasTls: options.tls === true || options.tlsCert !== undefined,
|
|
270
|
+
allowAnonymous,
|
|
271
|
+
});
|
|
272
|
+
if (preflight !== undefined) {
|
|
273
|
+
throw new Error(preflight);
|
|
274
|
+
}
|
|
275
|
+
const tls: ServeTaskTls | undefined =
|
|
276
|
+
options.tls === true
|
|
277
|
+
? { mode: "self-signed", sanEntries: tlsSan }
|
|
278
|
+
: options.tlsCert !== undefined && options.tlsKey !== undefined
|
|
279
|
+
? { mode: "certificates", certPath: options.tlsCert, keyPath: options.tlsKey }
|
|
280
|
+
: undefined;
|
|
281
|
+
return {
|
|
282
|
+
exitCode: 0,
|
|
283
|
+
stdout: renderServeTaskArtifacts({
|
|
284
|
+
host,
|
|
285
|
+
port,
|
|
286
|
+
...(endpoint !== undefined ? { endpoint } : {}),
|
|
287
|
+
...(tls !== undefined ? { tls } : {}),
|
|
288
|
+
}),
|
|
289
|
+
stderr: "",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
247
292
|
const tokens = context.tokens ?? parseServeTokens(process.env.UKP_SERVE_TOKEN);
|
|
248
293
|
// W10: socket activation must not admit tokenless serving — the bind
|
|
249
294
|
// address belongs to the socket unit (possibly public), so the loopback
|
|
250
295
|
// premise of --allow-anonymous is unverifiable here (RQ-18 review P1).
|
|
251
296
|
if (systemdSocket && tokens.length === 0) {
|
|
252
297
|
throw new Error(
|
|
253
|
-
"refusing to serve on --systemd-socket without a token: the listener's bind address belongs to the socket unit and may be public
|
|
298
|
+
"refusing to serve on --systemd-socket without a token: the listener's bind address belongs to the socket unit and may be public - tokenless loopback serving cannot be verified here; set UKP_SERVE_TOKEN",
|
|
254
299
|
);
|
|
255
300
|
}
|
|
256
301
|
const decision = serveAuthDecision(host, tokens, allowAnonymous);
|
package/src/registry.ts
CHANGED
|
@@ -230,7 +230,7 @@ export function assertRegistrableRemoteUrl(raw: string): void {
|
|
|
230
230
|
+ `${parts.endpointName !== undefined ? `/${parts.endpointName}` : ""}`;
|
|
231
231
|
throw new RegistryError(
|
|
232
232
|
`ssh:// urls take no port: the woken door listens on a client-chosen loopback port and the url port selects nothing`
|
|
233
|
-
+ `
|
|
233
|
+
+ ` - register '${portless}' instead`
|
|
234
234
|
+ ` (for a non-standard sshd port, use an ssh config Host alias)`,
|
|
235
235
|
);
|
|
236
236
|
}
|
|
@@ -240,7 +240,7 @@ export class RegistryBusyError extends RegistryError {
|
|
|
240
240
|
constructor(path: string) {
|
|
241
241
|
super(
|
|
242
242
|
`registry_busy: ${path} (another ukp process holds the registry lock; ` +
|
|
243
|
-
`stale locks are reclaimed automatically
|
|
243
|
+
`stale locks are reclaimed automatically - close other ukp commands or retry shortly)`,
|
|
244
244
|
);
|
|
245
245
|
this.name = "RegistryBusyError";
|
|
246
246
|
}
|
package/src/server.ts
CHANGED
|
@@ -407,7 +407,7 @@ function parseReadParams(
|
|
|
407
407
|
pin: string | null,
|
|
408
408
|
): { request: ReadRequest } | { error: string; class?: string } {
|
|
409
409
|
if ((ref === null) === (uri === null)) {
|
|
410
|
-
return { error: "exactly one of 'ref' (endpoint-relative) or 'uri' (ukp
|
|
410
|
+
return { error: "exactly one of 'ref' (endpoint-relative) or 'uri' (ukp://...) is required" };
|
|
411
411
|
}
|
|
412
412
|
const args = uri !== null
|
|
413
413
|
? [
|
|
@@ -546,14 +546,14 @@ export function parseListenFds(env: NodeJS.ProcessEnv): { fd: number } | { error
|
|
|
546
546
|
};
|
|
547
547
|
}
|
|
548
548
|
if (Number(env.LISTEN_PID) !== process.pid) {
|
|
549
|
-
return { error: `LISTEN_PID ${env.LISTEN_PID} does not match this process (${process.pid})
|
|
549
|
+
return { error: `LISTEN_PID ${env.LISTEN_PID} does not match this process (${process.pid}) - inherited activation variables belong to another process` };
|
|
550
550
|
}
|
|
551
551
|
const count = Number(env.LISTEN_FDS);
|
|
552
552
|
if (!Number.isSafeInteger(count) || count < 1) {
|
|
553
553
|
return { error: `LISTEN_FDS=${String(env.LISTEN_FDS)} is not a positive integer` };
|
|
554
554
|
}
|
|
555
555
|
if (count !== 1) {
|
|
556
|
-
return { error: `expected exactly one listening fd (LISTEN_FDS=1), got ${count}
|
|
556
|
+
return { error: `expected exactly one listening fd (LISTEN_FDS=1), got ${count} - run one socket unit per serve instance` };
|
|
557
557
|
}
|
|
558
558
|
delete env.LISTEN_FDS;
|
|
559
559
|
delete env.LISTEN_PID;
|
|
@@ -645,7 +645,7 @@ function startServeListener(
|
|
|
645
645
|
isSocket = false;
|
|
646
646
|
}
|
|
647
647
|
if (!isSocket) {
|
|
648
|
-
throw new ServeSetupError(`fd ${parsed.fd} is not an open socket
|
|
648
|
+
throw new ServeSetupError(`fd ${parsed.fd} is not an open socket - socket activation must pass a bound listening socket`);
|
|
649
649
|
}
|
|
650
650
|
const server = Bun.serve({ hostname: "127.0.0.1", port: 0, ...tls, fetch });
|
|
651
651
|
const acceptor = netCreateServer();
|