centralu 0.1.0-beta.44 → 0.1.0-beta.46
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/bin/centralu.mjs +7 -1
- package/bin/serve-args.mjs +57 -8
- package/bin/serve-autostart.mjs +6 -5
- package/bin/serve-detach.mjs +2 -6
- package/bin/serve-files.mjs +433 -55
- package/bin/serve-handover.mjs +55 -18
- package/bin/serve-host.mjs +126 -54
- package/bin/serve-keys.mjs +126 -0
- package/bin/serve-run.mjs +112 -24
- package/bin/serve-util.mjs +7 -1
- package/bin/serve-window.mjs +10 -6
- package/bin/serve-windows.mjs +10 -0
- package/bin/serve.mjs +6 -5
- package/package.json +5 -5
package/bin/centralu.mjs
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
parseServeArgs,
|
|
49
49
|
printConnection,
|
|
50
50
|
rotateToken,
|
|
51
|
+
revokeCredential,
|
|
51
52
|
runAutostart,
|
|
52
53
|
runDetach,
|
|
53
54
|
runDetachedChild,
|
|
@@ -621,6 +622,11 @@ switch (cmd) {
|
|
|
621
622
|
break
|
|
622
623
|
}
|
|
623
624
|
try {
|
|
625
|
+
// One full credential withdrawn (#1405): like --rotate-token, nothing to start or describe
|
|
626
|
+
if (args.mode === 'revoke') {
|
|
627
|
+
process.exitCode = revokeCredential({ env: process.env, home: HOME, id: args.revoke })
|
|
628
|
+
break
|
|
629
|
+
}
|
|
624
630
|
if (args.mode === 'rotate') {
|
|
625
631
|
process.exitCode = rotateToken({ env: process.env, home: HOME })
|
|
626
632
|
break
|
|
@@ -655,7 +661,7 @@ switch (cmd) {
|
|
|
655
661
|
if (spec) Object.assign(process.env, spec.env)
|
|
656
662
|
// realpath: npm's bin is a symlink to this file, and the launcher must not depend on the link
|
|
657
663
|
const opts = { env: process.env, home: HOME, entry: requireHostEntry(), version: pkg.version, cliPath: realpathSync(process.argv[1]) }
|
|
658
|
-
if (args.mode === 'connection') process.exitCode = await printConnection(opts)
|
|
664
|
+
if (args.mode === 'connection') process.exitCode = await printConnection({ ...opts, pair: args.pair ?? null, pairKey: args.pairKey ?? null, keyPaired: args.keyPaired === true })
|
|
659
665
|
else if (args.mode === 'detach') process.exitCode = await runDetach({ ...opts, port: args.port, from: args.from })
|
|
660
666
|
else if (args.mode === 'stop') process.exitCode = await runStop(opts)
|
|
661
667
|
else if (args.mode === 'window') process.exitCode = await runWindow(opts)
|
package/bin/serve-args.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The arguments after `centralu serve`, and its help */
|
|
2
2
|
import { DEFAULT_SERVE_PORT } from './serve-port.mjs'
|
|
3
|
+
import { parseLinkPublicKey } from './serve-keys.mjs'
|
|
3
4
|
|
|
4
5
|
export const SERVE_HELP = `centralu serve: run the Centralu host on this machine, without a window
|
|
5
6
|
|
|
@@ -8,10 +9,17 @@ export const SERVE_HELP = `centralu serve: run the Centralu host on this machine
|
|
|
8
9
|
centralu serve --detach Start it in the background, outside this session; return once it answers
|
|
9
10
|
centralu serve --stop Ask the running host to stop, and wait until it has
|
|
10
11
|
centralu serve --window What the Centralu window on Windows runs here: start the host if
|
|
11
|
-
none runs, print its port and
|
|
12
|
-
stdin ends, and leave the host running
|
|
13
|
-
centralu serve --connection Print how to reach it as one JSON line, then exit
|
|
14
|
-
|
|
12
|
+
none runs, print its port and a full credential for the window as
|
|
13
|
+
a ready line, stay until stdin ends, and leave the host running
|
|
14
|
+
centralu serve --connection Print how to reach it as one JSON line, then exit. It carries the
|
|
15
|
+
limited credential (everyday calls) and lists the full ones
|
|
16
|
+
centralu serve --connection --pair <label>
|
|
17
|
+
... and a new full credential for <label>, printed once and kept
|
|
18
|
+
here only as its hash; it replaces <label>'s earlier one. What the
|
|
19
|
+
app on another computer runs at each link
|
|
20
|
+
centralu serve --revoke <id> Withdraw one full credential (--connection lists their ids)
|
|
21
|
+
centralu serve --rotate-token A new key, and every full credential withdrawn (a running serve
|
|
22
|
+
keeps its old key until restarted)
|
|
15
23
|
centralu serve --from <hub> With serve or --detach: the name of the computer whose link asks,
|
|
16
24
|
said by a second Centralu on this data folder when it is refused
|
|
17
25
|
centralu serve --autostart on|off|remove|status
|
|
@@ -32,7 +40,16 @@ from an install the app made over SSH, which has a launcher of its own.`
|
|
|
32
40
|
|
|
33
41
|
/** The flags that each make serve a different command */
|
|
34
42
|
const MODE_FLAGS = { '--connection': 'connection', '--rotate-token': 'rotate', '--detach': 'detach', '--stop': 'stop', '--window': 'window' }
|
|
35
|
-
const FLAG_OF = { ...Object.fromEntries(Object.entries(MODE_FLAGS).map(([f, m]) => [m, f])), autostart: '--autostart' }
|
|
43
|
+
const FLAG_OF = { ...Object.fromEntries(Object.entries(MODE_FLAGS).map(([f, m]) => [m, f])), autostart: '--autostart', revoke: '--revoke' }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What `--pair` takes (#1405): who the full credential is for, said back in `--connection`'s `others` and on the hub's
|
|
47
|
+
* machine row. A hub sends `hub:<its machine id>`
|
|
48
|
+
*/
|
|
49
|
+
export const PAIR_LABEL_RE = /^[A-Za-z0-9:._-]{1,80}$/
|
|
50
|
+
|
|
51
|
+
/** What `--revoke` takes: a full credential's id, as `--connection` lists it (8 hex characters) */
|
|
52
|
+
export const CREDENTIAL_ID_RE = /^[0-9a-f]{8}$/
|
|
36
53
|
|
|
37
54
|
/** What `--autostart` takes */
|
|
38
55
|
export const AUTOSTART_ACTIONS = ['on', 'off', 'remove', 'status']
|
|
@@ -66,9 +83,36 @@ export function parseServeArgs(argv) {
|
|
|
66
83
|
let child = null
|
|
67
84
|
let autostart = null
|
|
68
85
|
let from = null
|
|
86
|
+
let pair = null
|
|
87
|
+
let pairKey = null
|
|
88
|
+
let keyPaired = false
|
|
89
|
+
let revoke = null
|
|
69
90
|
for (let i = 0; i < argv.length; i++) {
|
|
70
91
|
const a = argv[i]
|
|
71
|
-
if (a === '--
|
|
92
|
+
if (a === '--pair' || a.startsWith('--pair=')) {
|
|
93
|
+
const raw = a === '--pair' ? argv[++i] : a.slice('--pair='.length)
|
|
94
|
+
if (typeof raw !== 'string' || !PAIR_LABEL_RE.test(raw)) return { error: `--pair needs a label: letters, digits, ':', '.', '_' and '-' (got ${raw === undefined ? 'nothing' : JSON.stringify(raw)})` }
|
|
95
|
+
pair = raw
|
|
96
|
+
} else if (a === '--pair-key') {
|
|
97
|
+
/*
|
|
98
|
+
* `--pair-key <label> <public key>` (#1711, docs/plans/key-pairing.md §4.2): a hub's key pairing, at each link in
|
|
99
|
+
* place of `--pair`. The key is checked as the host checks it (§4.1): a refused one writes nothing
|
|
100
|
+
*/
|
|
101
|
+
const label = argv[++i]
|
|
102
|
+
const publicKey = argv[++i]
|
|
103
|
+
if (typeof label !== 'string' || !PAIR_LABEL_RE.test(label)) return { error: `--pair-key needs a label: letters, digits, ':', '.', '_' and '-' (got ${label === undefined ? 'nothing' : JSON.stringify(label)})` }
|
|
104
|
+
if (!parseLinkPublicKey(publicKey)) return { error: `--pair-key needs an Ed25519 public key after the label, as 43 characters of base64url (got ${publicKey === undefined ? 'nothing' : JSON.stringify(String(publicKey).slice(0, 64))})` }
|
|
105
|
+
pairKey = { label, publicKey }
|
|
106
|
+
} else if (a === '--key-paired') {
|
|
107
|
+
// The hub has paired by key with this machine before (#1711, §4.2): a key line removes the label's verifier entry
|
|
108
|
+
keyPaired = true
|
|
109
|
+
} else if (a === '--revoke' || a.startsWith('--revoke=')) {
|
|
110
|
+
const raw = a === '--revoke' ? argv[++i] : a.slice('--revoke='.length)
|
|
111
|
+
if (typeof raw !== 'string' || !CREDENTIAL_ID_RE.test(raw)) return { error: `--revoke needs a credential's id, as --connection lists it (got ${raw === undefined ? 'nothing' : JSON.stringify(raw)})` }
|
|
112
|
+
if (mode !== 'serve' && mode !== 'revoke') return { error: `${FLAG_OF[mode]} and --revoke are separate commands; pass one` }
|
|
113
|
+
mode = 'revoke'
|
|
114
|
+
revoke = raw
|
|
115
|
+
} else if (a === '--autostart' || a.startsWith('--autostart=')) {
|
|
72
116
|
const raw = a === '--autostart' ? argv[++i] : a.slice('--autostart='.length)
|
|
73
117
|
if (!AUTOSTART_ACTIONS.includes(raw)) return { error: `--autostart needs on, off, remove or status (got ${raw === undefined ? 'nothing' : JSON.stringify(raw)})` }
|
|
74
118
|
if (mode !== 'serve' && mode !== 'autostart') return { error: `${FLAG_OF[mode]} and --autostart are separate commands; pass one` }
|
|
@@ -76,7 +120,7 @@ export function parseServeArgs(argv) {
|
|
|
76
120
|
autostart = raw
|
|
77
121
|
} else if (Object.hasOwn(MODE_FLAGS, a)) {
|
|
78
122
|
const m = MODE_FLAGS[a]
|
|
79
|
-
if (mode !== 'serve' && mode !== m) return { error: `${FLAG_OF[mode]} and ${a} are separate commands; pass one` }
|
|
123
|
+
if (mode !== 'serve' && mode !== m) return { error: `${FLAG_OF[mode] ?? mode} and ${a} are separate commands; pass one` }
|
|
80
124
|
mode = m
|
|
81
125
|
} else if (a === '--help' || a === '-h') help = true
|
|
82
126
|
else if (a.startsWith(CHILD_FLAG)) child = a.slice(CHILD_FLAG.length)
|
|
@@ -92,6 +136,11 @@ export function parseServeArgs(argv) {
|
|
|
92
136
|
} else return { error: `unknown option for serve: ${a}\n\n${SERVE_HELP}` }
|
|
93
137
|
}
|
|
94
138
|
if (help) return { mode: 'help', port }
|
|
139
|
+
if (pair !== null && mode !== 'connection') return { error: '--pair goes with --connection' }
|
|
140
|
+
if (pairKey !== null && mode !== 'connection') return { error: '--pair-key goes with --connection' }
|
|
141
|
+
if (pair !== null && pairKey !== null) return { error: '--pair and --pair-key are two ways of pairing; pass one' }
|
|
142
|
+
if (keyPaired && pairKey === null) return { error: '--key-paired goes with --pair-key' }
|
|
143
|
+
if (mode === 'revoke') return port !== null ? { error: '--revoke takes no --port' } : { mode, port, revoke }
|
|
95
144
|
if (from !== null && mode !== 'serve' && mode !== 'detach') return { error: `--from goes with \`centralu serve\` and --detach, which start a host; not with ${FLAG_OF[mode]}` }
|
|
96
145
|
if (child !== null) {
|
|
97
146
|
if (mode !== 'serve') return { error: `${CHILD_FLAG.slice(0, -1)} is how --detach starts serve, not an option of ${FLAG_OF[mode]}` }
|
|
@@ -106,7 +155,7 @@ export function parseServeArgs(argv) {
|
|
|
106
155
|
if (port !== null) return { error: '--autostart takes no --port: the entry it writes starts serve on the port serve recorded' }
|
|
107
156
|
return { mode, port, autostart }
|
|
108
157
|
}
|
|
109
|
-
return { mode, port, ...(from !== null ? { from } : {}) }
|
|
158
|
+
return { mode, port, ...(from !== null ? { from } : {}), ...(pair !== null ? { pair } : {}), ...(pairKey !== null ? { pairKey, keyPaired } : {}) }
|
|
110
159
|
}
|
|
111
160
|
|
|
112
161
|
function parsePort(raw) {
|
package/bin/serve-autostart.mjs
CHANGED
|
@@ -384,10 +384,10 @@ async function systemdAutostart(action, { dataDir, env, home, name, run, user })
|
|
|
384
384
|
return { ...base, on: true, linger, ...(linger === true ? {} : { message: lingerMessage(user) }) }
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
async function taskAutostart(action, { dataDir, env, home, name, run, user, platform, interop }) {
|
|
387
|
+
async function taskAutostart(action, { dataDir, env, home, name, run, user, platform, interop, mounts }) {
|
|
388
388
|
const legacy = legacyTaskName({ dataDir, home, env, platform })
|
|
389
389
|
const detach = detachHow(platform, env, interop)
|
|
390
|
-
const ps = windowsPowershell(detach, env)
|
|
390
|
+
const ps = windowsPowershell(detach, env, mounts)
|
|
391
391
|
const base = { ok: true, how: 'task', linger: null }
|
|
392
392
|
if (!ps) return { ...base, ok: false, on: false, message: 'Windows PowerShell is not reachable from this WSL distro' }
|
|
393
393
|
const ask = (script) => askPowershell(ps, script, env, parseTaskAnswer, run)
|
|
@@ -456,8 +456,8 @@ export async function startInSignIn({ dataDir, env, home, platform = process.pla
|
|
|
456
456
|
* hub asks (the link started it), and the entry is for the next boot.
|
|
457
457
|
*
|
|
458
458
|
* stdout: one line, `{ v: 1, autostart: { ok, on, how, linger, message? } }`; `how` null where this
|
|
459
|
-
* machine offers nothing (`autostartHow`). `run` (a command runner) and `
|
|
460
|
-
* tests never reach the real systemd or Task Scheduler.
|
|
459
|
+
* machine offers nothing (`autostartHow`). `run` (a command runner), `how` and `mounts` (what /proc/mounts says, where WSL finds
|
|
460
|
+
* the Windows drive) are parameters so tests never reach the real systemd or Task Scheduler, nor depend on the drives of the machine.
|
|
461
461
|
*/
|
|
462
462
|
export async function runAutostart({
|
|
463
463
|
env,
|
|
@@ -467,6 +467,7 @@ export async function runAutostart({
|
|
|
467
467
|
platform = process.platform,
|
|
468
468
|
interop = wslInterop,
|
|
469
469
|
how = autostartHow(platform, env, { interop }),
|
|
470
|
+
mounts = undefined,
|
|
470
471
|
run = runCaptured,
|
|
471
472
|
user = userInfo().username,
|
|
472
473
|
write = (line) => process.stdout.write(line),
|
|
@@ -481,7 +482,7 @@ export async function runAutostart({
|
|
|
481
482
|
}
|
|
482
483
|
try {
|
|
483
484
|
const name = autostartName(how, { dataDir, home, env, platform, user })
|
|
484
|
-
const opts = { dataDir, env, home, name, run, user, platform, interop }
|
|
485
|
+
const opts = { dataDir, env, home, name, run, user, platform, interop, mounts }
|
|
485
486
|
return report(how === 'systemd' ? await systemdAutostart(action, opts) : await taskAutostart(action, opts))
|
|
486
487
|
} catch (e) {
|
|
487
488
|
return report({ ok: false, on: readAutostartMark(dataDir)?.how === how, how, linger: null, message: e?.message ?? String(e) })
|
package/bin/serve-detach.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { userInfo } from 'node:os'
|
|
|
8
8
|
import { join } from 'node:path'
|
|
9
9
|
import { answerLine, isAlive, say, sayTo, runCaptured, tail, sleep } from './serve-util.mjs'
|
|
10
10
|
import { SERVE_LOG_FILE, serveDataDir, ensureServeState, markStopped, rotateServeLog } from './serve-files.mjs'
|
|
11
|
-
import { askPowershell, detachHow, windowsArg, wmiScript, parseWmiAnswer, wmiBlockedMessage, windowsPowershell, windowsTool,
|
|
11
|
+
import { askPowershell, detachHow, windowsArg, wmiScript, parseWmiAnswer, wmiBlockedMessage, windowsPowershell, windowsTool, wslCommandLine } from './serve-windows.mjs'
|
|
12
12
|
import { CHILD_FLAG } from './serve-args.mjs'
|
|
13
13
|
import { startInSignIn } from './serve-autostart.mjs'
|
|
14
14
|
import { stopServedHost } from './serve-handover.mjs'
|
|
@@ -72,11 +72,7 @@ export async function startDetached({ how, execPath, args, env, cwd }) {
|
|
|
72
72
|
let commandLine
|
|
73
73
|
try {
|
|
74
74
|
if (how === 'wmi') commandLine = [execPath, ...args].map(windowsArg).join(' ')
|
|
75
|
-
else {
|
|
76
|
-
const distro = wslDistro(env.WSL_DISTRO_NAME ?? '')
|
|
77
|
-
const user = wslUser(userInfo().username)
|
|
78
|
-
commandLine = ['wsl.exe', '-d', distro, '-u', user, '--cd', cwd, '--exec', execPath, ...args].map(windowsArg).join(' ')
|
|
79
|
-
}
|
|
75
|
+
else commandLine = wslCommandLine({ distro: env.WSL_DISTRO_NAME ?? '', user: userInfo().username, cwd, execPath, args })
|
|
80
76
|
} catch (e) {
|
|
81
77
|
return { ok: false, blocked: false, message: e.message }
|
|
82
78
|
}
|