@cpajs/cpa 0.0.1-alpha-3 → 0.1.0-rc.2
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 +108 -12
- package/dist/client/resources/oauth.d.ts +6 -2
- package/dist/client/resources/oauth.d.ts.map +1 -1
- package/dist/client/resources/oauth.js +28 -8
- package/dist/client/resources/oauth.js.map +1 -1
- package/dist/client/transport.d.ts.map +1 -1
- package/dist/client/transport.js +7 -3
- package/dist/client/transport.js.map +1 -1
- package/dist/server/args.js +1 -1
- package/dist/server/args.js.map +1 -1
- package/dist/server/binary.d.ts +19 -0
- package/dist/server/binary.d.ts.map +1 -1
- package/dist/server/binary.js +35 -1
- package/dist/server/binary.js.map +1 -1
- package/dist/server/config.d.ts +46 -7
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +148 -22
- package/dist/server/config.js.map +1 -1
- package/dist/server/errors.d.ts +4 -2
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +1 -1
- package/dist/server/errors.js.map +1 -1
- package/dist/server/index.d.ts +18 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +383 -79
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +19 -0
- package/dist/server/internal.d.ts.map +1 -0
- package/dist/server/internal.js +10 -0
- package/dist/server/internal.js.map +1 -0
- package/dist/server/management.d.ts +29 -0
- package/dist/server/management.d.ts.map +1 -0
- package/dist/server/management.js +64 -0
- package/dist/server/management.js.map +1 -0
- package/dist/server/process.d.ts +12 -1
- package/dist/server/process.d.ts.map +1 -1
- package/dist/server/process.js +55 -13
- package/dist/server/process.js.map +1 -1
- package/dist/server/readiness.d.ts +16 -2
- package/dist/server/readiness.d.ts.map +1 -1
- package/dist/server/readiness.js +32 -2
- package/dist/server/readiness.js.map +1 -1
- package/dist/server/targets.d.ts +4 -0
- package/dist/server/targets.d.ts.map +1 -1
- package/dist/server/targets.js +6 -0
- package/dist/server/targets.js.map +1 -1
- package/dist/server/types.d.ts +88 -14
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +31 -24
- package/src/client/resources/oauth.ts +28 -13
- package/src/client/transport.ts +7 -3
- package/src/server/args.ts +1 -1
- package/src/server/binary.ts +46 -1
- package/src/server/config.ts +169 -22
- package/src/server/errors.ts +4 -2
- package/src/server/index.ts +459 -94
- package/src/server/internal.ts +20 -0
- package/src/server/management.ts +89 -0
- package/src/server/process.ts +60 -13
- package/src/server/readiness.ts +38 -3
- package/src/server/targets.ts +8 -0
- package/src/server/types.ts +90 -14
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { compare, hash } from 'bcryptjs';
|
|
2
|
+
import { CpaStartError } from './errors.ts';
|
|
3
|
+
import type { ManagementOptions } from './types.ts';
|
|
4
|
+
|
|
5
|
+
/** bcrypt cost; matches upstream's `bcrypt.DefaultCost` (about 50-100 ms per hash). */
|
|
6
|
+
export const BCRYPT_COST = 10;
|
|
7
|
+
|
|
8
|
+
// bcrypt only uses the first 72 bytes and upstream's Go implementation rejects
|
|
9
|
+
// longer passwords, so a hash of a longer key could never authenticate.
|
|
10
|
+
const MAX_BCRYPT_BYTES = 72;
|
|
11
|
+
|
|
12
|
+
export interface ResolvedManagement {
|
|
13
|
+
/** The plaintext key, when the caller supplied one. */
|
|
14
|
+
key: string | undefined;
|
|
15
|
+
/** True when the key travels through `MANAGEMENT_PASSWORD`. */
|
|
16
|
+
viaEnvironment: boolean;
|
|
17
|
+
/** Value for `management.allow-remote` in a managed config. */
|
|
18
|
+
allowRemote: boolean;
|
|
19
|
+
/** bcrypt hash for `management.secret-key` (default mode only). */
|
|
20
|
+
secretKeyHash: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalises the management options and hashes the key for the default mode.
|
|
25
|
+
* The deprecated `managementKey` alias maps to the same safe default, not to
|
|
26
|
+
* the old `MANAGEMENT_PASSWORD` behaviour.
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveManagement(options: {
|
|
29
|
+
management?: ManagementOptions;
|
|
30
|
+
managementKey?: string;
|
|
31
|
+
}): Promise<ResolvedManagement> {
|
|
32
|
+
if (options.management !== undefined && options.managementKey !== undefined) {
|
|
33
|
+
throw new CpaStartError(
|
|
34
|
+
'invalid-options',
|
|
35
|
+
'management and managementKey are mutually exclusive; managementKey is the deprecated alias for management.key.',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const management =
|
|
39
|
+
options.management ?? (options.managementKey === undefined ? undefined : { key: options.managementKey });
|
|
40
|
+
if (management === undefined) {
|
|
41
|
+
return {
|
|
42
|
+
key: undefined,
|
|
43
|
+
viaEnvironment: false,
|
|
44
|
+
allowRemote: false,
|
|
45
|
+
secretKeyHash: undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const key = management.key;
|
|
49
|
+
if (typeof key !== 'string' || key.trim() === '') {
|
|
50
|
+
throw new CpaStartError('invalid-options', 'management.key must not be empty.');
|
|
51
|
+
}
|
|
52
|
+
if (management.viaEnvironment === true) {
|
|
53
|
+
// Upstream trims MANAGEMENT_PASSWORD, so a key with surrounding whitespace
|
|
54
|
+
// would not match what the caller sends from its own client.
|
|
55
|
+
if (key !== key.trim()) {
|
|
56
|
+
throw new CpaStartError('invalid-options', 'management.key must not start or end with whitespace when viaEnvironment is true.');
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
key,
|
|
60
|
+
viaEnvironment: true,
|
|
61
|
+
allowRemote: management.allowRemote === true,
|
|
62
|
+
secretKeyHash: undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (Buffer.byteLength(key, 'utf8') > MAX_BCRYPT_BYTES) {
|
|
66
|
+
throw new CpaStartError(
|
|
67
|
+
'invalid-options',
|
|
68
|
+
`management.key is longer than ${MAX_BCRYPT_BYTES} bytes, which bcrypt cannot represent. Use management.viaEnvironment: true for a longer key.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
key,
|
|
73
|
+
viaEnvironment: false,
|
|
74
|
+
allowRemote: management.allowRemote === true,
|
|
75
|
+
secretKeyHash: await hash(key, BCRYPT_COST),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const BCRYPT_HASH = /^\$2[aby]\$/;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* True when `stored` (a `management.secret-key` value) authenticates `key` the
|
|
83
|
+
* way upstream checks it: bcrypt hashes are compared, a plaintext value is
|
|
84
|
+
* hashed by CPA on load and so must equal the key.
|
|
85
|
+
*/
|
|
86
|
+
export async function keyMatches(key: string, stored: string | undefined): Promise<boolean> {
|
|
87
|
+
if (stored === undefined) return false;
|
|
88
|
+
return BCRYPT_HASH.test(stored) ? compare(key, stored) : stored === key;
|
|
89
|
+
}
|
package/src/server/process.ts
CHANGED
|
@@ -3,16 +3,23 @@ import type { Readable } from 'node:stream';
|
|
|
3
3
|
import type { ExitResult, LogLine } from './types.ts';
|
|
4
4
|
|
|
5
5
|
const MAX_RECENT_LINES = 200;
|
|
6
|
+
/** Longest line kept, in UTF-16 code units; longer output is truncated with a marker. */
|
|
7
|
+
export const MAX_LINE_LENGTH = 64 * 1024;
|
|
6
8
|
|
|
7
|
-
// Children still running when
|
|
8
|
-
|
|
9
|
+
// Children still running when this process exits normally (or calls
|
|
10
|
+
// process.exit()) are killed. A fatal signal with no listener, e.g. a default
|
|
11
|
+
// SIGTERM or SIGKILL, ends Node without emitting 'exit'; there the launcher
|
|
12
|
+
// (cpa-launcher, see binary.ts) stops CPA instead. This library does not
|
|
13
|
+
// install signal handlers, because doing so would change how the host process
|
|
14
|
+
// reacts to them.
|
|
15
|
+
const live = new Set<ManagedProcess>();
|
|
9
16
|
let exitHookInstalled = false;
|
|
10
|
-
function
|
|
11
|
-
live.add(
|
|
17
|
+
function track(proc: ManagedProcess): void {
|
|
18
|
+
live.add(proc);
|
|
12
19
|
if (exitHookInstalled) return;
|
|
13
20
|
exitHookInstalled = true;
|
|
14
21
|
process.once('exit', () => {
|
|
15
|
-
for (const
|
|
22
|
+
for (const p of live) p.kill('SIGKILL');
|
|
16
23
|
});
|
|
17
24
|
}
|
|
18
25
|
|
|
@@ -21,6 +28,12 @@ export interface SpawnOptions {
|
|
|
21
28
|
env: NodeJS.ProcessEnv;
|
|
22
29
|
redact: (text: string) => string;
|
|
23
30
|
onLog?: ((log: LogLine) => void) | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Supervise mode of cpa-launcher: start it in its own process group, signal
|
|
33
|
+
* the whole group (launcher and CPA), and keep a stdin pipe open whose EOF
|
|
34
|
+
* tells the launcher that this process has died.
|
|
35
|
+
*/
|
|
36
|
+
supervised?: boolean;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
/** A spawned CPA process with line-buffered, redacted output and idempotent stop. */
|
|
@@ -35,18 +48,26 @@ export class ManagedProcess {
|
|
|
35
48
|
readonly #redact: (text: string) => string;
|
|
36
49
|
readonly #onLog: ((log: LogLine) => void) | undefined;
|
|
37
50
|
readonly #lineListeners = new Set<(log: LogLine) => void>();
|
|
51
|
+
readonly #supervised: boolean;
|
|
38
52
|
#stopping: Promise<ExitResult> | undefined;
|
|
39
53
|
|
|
40
54
|
constructor(binaryPath: string, args: readonly string[], options: SpawnOptions) {
|
|
41
55
|
this.#redact = options.redact;
|
|
42
56
|
this.#onLog = options.onLog;
|
|
57
|
+
this.#supervised = options.supervised === true;
|
|
43
58
|
this.child = spawn(binaryPath, [...args], {
|
|
44
59
|
cwd: options.cwd,
|
|
45
60
|
env: options.env,
|
|
46
|
-
|
|
61
|
+
// The supervised launcher's stdin is a liveness pipe: never written, closed by
|
|
62
|
+
// the OS when this process dies.
|
|
63
|
+
stdio: [this.#supervised ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
64
|
+
// Its own process group, so stop() can signal the launcher and CPA together.
|
|
65
|
+
detached: this.#supervised,
|
|
47
66
|
windowsHide: true,
|
|
48
67
|
});
|
|
49
|
-
|
|
68
|
+
// A liveness pipe with nobody reading it is expected after the launcher exits.
|
|
69
|
+
this.child.stdin?.on('error', () => {});
|
|
70
|
+
track(this);
|
|
50
71
|
|
|
51
72
|
this.exited = new Promise<ExitResult>((resolve) => {
|
|
52
73
|
this.child.once('error', (error) => {
|
|
@@ -64,7 +85,7 @@ export class ManagedProcess {
|
|
|
64
85
|
#finish(resolve: (result: ExitResult) => void, result: ExitResult): void {
|
|
65
86
|
if (this.exitResult) return;
|
|
66
87
|
this.exitResult = result;
|
|
67
|
-
live.delete(this
|
|
88
|
+
live.delete(this);
|
|
68
89
|
resolve(result);
|
|
69
90
|
}
|
|
70
91
|
|
|
@@ -72,8 +93,14 @@ export class ManagedProcess {
|
|
|
72
93
|
if (!stream) return;
|
|
73
94
|
stream.setEncoding('utf8');
|
|
74
95
|
let pending = '';
|
|
96
|
+
// Characters dropped from the line currently being received; bounds memory
|
|
97
|
+
// when the child writes very long or never-terminated lines.
|
|
98
|
+
let dropped = 0;
|
|
75
99
|
const emit = (raw: string): void => {
|
|
76
|
-
const
|
|
100
|
+
const text = raw.replace(/\r$/, '');
|
|
101
|
+
const line = dropped > 0 ? `${text} …[truncated ${dropped} chars]` : text;
|
|
102
|
+
dropped = 0;
|
|
103
|
+
const log: LogLine = { stream: name, line: this.#redact(line) };
|
|
77
104
|
this.recentLogs.push(log);
|
|
78
105
|
if (this.recentLogs.length > MAX_RECENT_LINES) this.recentLogs.shift();
|
|
79
106
|
for (const listener of this.#lineListeners) listener(log);
|
|
@@ -83,9 +110,14 @@ export class ManagedProcess {
|
|
|
83
110
|
pending += chunk;
|
|
84
111
|
let index: number;
|
|
85
112
|
while ((index = pending.indexOf('\n')) !== -1) {
|
|
86
|
-
|
|
113
|
+
if (index > MAX_LINE_LENGTH) dropped += index - MAX_LINE_LENGTH;
|
|
114
|
+
emit(pending.slice(0, Math.min(index, MAX_LINE_LENGTH)));
|
|
87
115
|
pending = pending.slice(index + 1);
|
|
88
116
|
}
|
|
117
|
+
if (pending.length > MAX_LINE_LENGTH) {
|
|
118
|
+
dropped += pending.length - MAX_LINE_LENGTH;
|
|
119
|
+
pending = pending.slice(0, MAX_LINE_LENGTH);
|
|
120
|
+
}
|
|
89
121
|
});
|
|
90
122
|
stream.on('end', () => {
|
|
91
123
|
if (pending) emit(pending);
|
|
@@ -93,6 +125,20 @@ export class ManagedProcess {
|
|
|
93
125
|
});
|
|
94
126
|
}
|
|
95
127
|
|
|
128
|
+
/** Signals the process, or the launcher's whole process group in supervise mode. */
|
|
129
|
+
kill(signal: NodeJS.Signals): void {
|
|
130
|
+
const pid = this.child.pid;
|
|
131
|
+
if (this.#supervised && pid !== undefined) {
|
|
132
|
+
try {
|
|
133
|
+
process.kill(-pid, signal);
|
|
134
|
+
return;
|
|
135
|
+
} catch {
|
|
136
|
+
// The group is already gone; fall back to the direct child.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this.child.kill(signal);
|
|
140
|
+
}
|
|
141
|
+
|
|
96
142
|
onLine(listener: (log: LogLine) => void): () => void {
|
|
97
143
|
this.#lineListeners.add(listener);
|
|
98
144
|
return () => this.#lineListeners.delete(listener);
|
|
@@ -104,19 +150,20 @@ export class ManagedProcess {
|
|
|
104
150
|
|
|
105
151
|
/**
|
|
106
152
|
* Sends SIGTERM and waits up to `timeoutMs` before SIGKILL. On Windows Node
|
|
107
|
-
*
|
|
153
|
+
* cannot deliver SIGTERM: both steps terminate the process forcefully, so
|
|
154
|
+
* there is no grace period and the timeout is irrelevant.
|
|
108
155
|
*/
|
|
109
156
|
stop(timeoutMs: number): Promise<ExitResult> {
|
|
110
157
|
if (this.exitResult) return Promise.resolve(this.exitResult);
|
|
111
158
|
this.#stopping ??= (async () => {
|
|
112
|
-
this.
|
|
159
|
+
this.kill('SIGTERM');
|
|
113
160
|
let timer: NodeJS.Timeout | undefined;
|
|
114
161
|
const timedOut = new Promise<'timeout'>((resolve) => {
|
|
115
162
|
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
116
163
|
});
|
|
117
164
|
const first = await Promise.race([this.exited, timedOut]);
|
|
118
165
|
clearTimeout(timer);
|
|
119
|
-
if (first === 'timeout') this.
|
|
166
|
+
if (first === 'timeout') this.kill('SIGKILL');
|
|
120
167
|
return this.exited;
|
|
121
168
|
})();
|
|
122
169
|
return this.#stopping;
|
package/src/server/readiness.ts
CHANGED
|
@@ -9,6 +9,15 @@ export function connectHost(bindHost: string): string {
|
|
|
9
9
|
return bindHost;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** True when a bind host only accepts loopback connections. */
|
|
13
|
+
export function isLoopbackHost(host: string): boolean {
|
|
14
|
+
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
15
|
+
if (normalized === '' || normalized === '0.0.0.0' || normalized === '::') return false;
|
|
16
|
+
if (normalized === 'localhost' || normalized === '::1') return true;
|
|
17
|
+
if (normalized.startsWith('::ffff:127.')) return true;
|
|
18
|
+
return /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export function formatOrigin(protocol: 'http' | 'https', host: string, port: number): string {
|
|
13
22
|
const h = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
14
23
|
return `${protocol}://${h}:${port}`;
|
|
@@ -32,8 +41,21 @@ export function probeHealth(origin: string, timeoutMs: number): Promise<boolean>
|
|
|
32
41
|
});
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
/**
|
|
36
|
-
export
|
|
44
|
+
/** A free port whose probing listener stays open until `release()` is called. */
|
|
45
|
+
export interface PortReservation {
|
|
46
|
+
port: number;
|
|
47
|
+
/** Closes the probing listener so a child process can bind the port. */
|
|
48
|
+
release(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reserves a currently free TCP port on `host`.
|
|
53
|
+
*
|
|
54
|
+
* The listening socket is left open until `release()` so another process cannot
|
|
55
|
+
* take the port while the caller finishes preparing the child. The window
|
|
56
|
+
* between `release()` and the child's own bind cannot be removed entirely.
|
|
57
|
+
*/
|
|
58
|
+
export function findFreePort(host: string): Promise<PortReservation> {
|
|
37
59
|
return new Promise((resolve, reject) => {
|
|
38
60
|
const server = createServer();
|
|
39
61
|
server.unref();
|
|
@@ -41,7 +63,20 @@ export function findFreePort(host: string): Promise<number> {
|
|
|
41
63
|
server.listen({ host: connectHost(host), port: 0, exclusive: true }, () => {
|
|
42
64
|
const address = server.address();
|
|
43
65
|
const port = typeof address === 'object' && address ? address.port : 0;
|
|
44
|
-
|
|
66
|
+
if (!port) {
|
|
67
|
+
server.close(() => reject(new Error('Could not allocate a free port.')));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let released: Promise<void> | undefined;
|
|
71
|
+
resolve({
|
|
72
|
+
port,
|
|
73
|
+
release() {
|
|
74
|
+
released ??= new Promise<void>((resolveClose) => {
|
|
75
|
+
server.close(() => resolveClose());
|
|
76
|
+
});
|
|
77
|
+
return released;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
45
80
|
});
|
|
46
81
|
});
|
|
47
82
|
}
|
package/src/server/targets.ts
CHANGED
|
@@ -131,3 +131,11 @@ export function packageName(target: Target): string {
|
|
|
131
131
|
export function binaryName(target: Pick<Target, 'os'>): string {
|
|
132
132
|
return target.os === 'win32' ? 'cli-proxy-api.exe' : 'cli-proxy-api';
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
/** Name of the launcher shipped next to the CPA binary. Windows needs none (see binary.ts). */
|
|
136
|
+
export const LAUNCHER_NAME = 'cpa-launcher';
|
|
137
|
+
|
|
138
|
+
/** True for targets whose platform package ships `cpa-launcher`. */
|
|
139
|
+
export function hasLauncher(target: Pick<Target, 'os'>): boolean {
|
|
140
|
+
return target.os !== 'win32';
|
|
141
|
+
}
|
package/src/server/types.ts
CHANGED
|
@@ -18,6 +18,23 @@ export interface HomeOptions {
|
|
|
18
18
|
disableClusterDiscovery?: boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface ManagementOptions {
|
|
22
|
+
/** Management API key. Empty keys are rejected. */
|
|
23
|
+
key: string;
|
|
24
|
+
/**
|
|
25
|
+
* Write `management.allow-remote`, which lets non-loopback clients use the
|
|
26
|
+
* management API. Default `false`; ignored by `viaEnvironment`, which always
|
|
27
|
+
* enables remote access upstream.
|
|
28
|
+
*/
|
|
29
|
+
allowRemote?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Pass the key to CPA through `MANAGEMENT_PASSWORD` instead of writing a
|
|
32
|
+
* bcrypt hash into the managed config. Upstream treats that variable as
|
|
33
|
+
* enabling remote management and compares the key in plaintext.
|
|
34
|
+
*/
|
|
35
|
+
viaEnvironment?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
interface CommonServerOptions {
|
|
22
39
|
/** Use this executable instead of the bundled platform package. */
|
|
23
40
|
binaryPath?: string;
|
|
@@ -26,10 +43,15 @@ interface CommonServerOptions {
|
|
|
26
43
|
* directory. When omitted, a temporary directory is created and removed on stop.
|
|
27
44
|
*/
|
|
28
45
|
stateDir?: string;
|
|
46
|
+
/** Management API key and how it reaches CPA. */
|
|
47
|
+
management?: ManagementOptions;
|
|
29
48
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
49
|
+
* Deprecated alias for `management: { key }`. It uses the safe default
|
|
50
|
+
* (bcrypt hash in the config, remote management disabled) and will be removed
|
|
51
|
+
* in a future release. Use `management.viaEnvironment` for the old
|
|
52
|
+
* `MANAGEMENT_PASSWORD` behaviour.
|
|
53
|
+
*
|
|
54
|
+
* @deprecated Use `management` instead.
|
|
33
55
|
*/
|
|
34
56
|
managementKey?: string;
|
|
35
57
|
/**
|
|
@@ -37,6 +59,22 @@ interface CommonServerOptions {
|
|
|
37
59
|
* (`-local-model`). Explicit `models.*` config sources still win.
|
|
38
60
|
*/
|
|
39
61
|
localModel?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Start CPA through `cpa-launcher` so it cannot outlive this process, even
|
|
64
|
+
* when this process is killed with SIGKILL or crashes. Default `true`.
|
|
65
|
+
*
|
|
66
|
+
* - Linux: the launcher sets PR_SET_PDEATHSIG and execs CPA (no extra process).
|
|
67
|
+
* - macOS / BSD: the launcher stays as CPA's parent and stops it when this
|
|
68
|
+
* process dies; `pid` is then the launcher's pid, and signals sent to it are
|
|
69
|
+
* forwarded to CPA.
|
|
70
|
+
* - Windows: not needed; Node already kills child processes with their parent.
|
|
71
|
+
*
|
|
72
|
+
* The launcher ships next to the CPA binary in every platform package. With a
|
|
73
|
+
* custom `binaryPath` it is looked up next to that binary and then in an
|
|
74
|
+
* installed platform package; if none is found CPA is started directly and a
|
|
75
|
+
* notice is logged through `onLog`.
|
|
76
|
+
*/
|
|
77
|
+
launcher?: boolean;
|
|
40
78
|
/** Run as a node of a CPA Home control plane. Home mode disables the local management API. */
|
|
41
79
|
home?: HomeOptions;
|
|
42
80
|
/**
|
|
@@ -55,25 +93,41 @@ interface CommonServerOptions {
|
|
|
55
93
|
onLog?: (log: LogLine) => void;
|
|
56
94
|
/** Additional strings to redact from logs and errors. */
|
|
57
95
|
redact?: readonly string[];
|
|
58
|
-
/** Default grace period for `stop()` before the process is killed. Default 10 000 ms. */
|
|
96
|
+
/** Default grace period for `stop()` before the process is killed. Ignored on Windows, where `stop()` terminates forcefully. Default 10 000 ms. */
|
|
59
97
|
stopTimeoutMs?: number;
|
|
60
98
|
}
|
|
61
99
|
|
|
62
100
|
export interface StartWithConfigOptions extends CommonServerOptions {
|
|
63
101
|
/**
|
|
64
|
-
* Native CPA configuration.
|
|
65
|
-
*
|
|
102
|
+
* Native CPA configuration.
|
|
103
|
+
*
|
|
104
|
+
* Written as YAML to `<stateDir>/config.yaml` only when that file does not
|
|
105
|
+
* exist. On later starts the existing file is reused, so changes made through
|
|
106
|
+
* the management API (or by CPA itself) survive a `stop()` / `startServer()`
|
|
107
|
+
* cycle. Set `overwriteConfig: true` to replace it with this object.
|
|
66
108
|
*/
|
|
67
109
|
config?: CpaConfig;
|
|
68
110
|
configPath?: never;
|
|
69
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Replace `<stateDir>/config.yaml` even when it already exists. Default
|
|
113
|
+
* `false`. When the existing file is kept, `config`, `host` and `port` are not
|
|
114
|
+
* applied and a message is emitted through `onLog`.
|
|
115
|
+
*/
|
|
116
|
+
overwriteConfig?: boolean;
|
|
117
|
+
/** Bind host, when cpajs writes the managed file. Default `config.server.host`, else `127.0.0.1`. */
|
|
70
118
|
host?: string;
|
|
71
|
-
/** Bind port.
|
|
119
|
+
/** Bind port, when cpajs writes the managed file. Default `config.server.port`, else `0` (a free port is picked). */
|
|
72
120
|
port?: number;
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
export interface StartWithConfigPathOptions extends CommonServerOptions {
|
|
76
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Existing CPA config file, handed to CPA as-is. CPA may rewrite it on
|
|
126
|
+
* startup: it hashes a plaintext `management.secret-key`, removes legacy
|
|
127
|
+
* keys that conflict with their v8 equivalents, and normalizes the layout.
|
|
128
|
+
* The file must set a fixed positive port; a missing or non-positive port
|
|
129
|
+
* makes CPA bind a random port it does not report.
|
|
130
|
+
*/
|
|
77
131
|
configPath: string;
|
|
78
132
|
config?: never;
|
|
79
133
|
host?: never;
|
|
@@ -88,10 +142,20 @@ export interface ExitResult {
|
|
|
88
142
|
}
|
|
89
143
|
|
|
90
144
|
export interface StopOptions {
|
|
91
|
-
/** Grace period after the termination signal before the process is killed. */
|
|
145
|
+
/** Grace period after the termination signal before the process is killed. Ignored on Windows, where the process is terminated forcefully right away. */
|
|
92
146
|
timeoutMs?: number;
|
|
93
147
|
}
|
|
94
148
|
|
|
149
|
+
export interface RestartOptions {
|
|
150
|
+
/**
|
|
151
|
+
* Grace period after the termination signal before the old process is killed.
|
|
152
|
+
* Defaults to `stopTimeoutMs`.
|
|
153
|
+
*/
|
|
154
|
+
timeoutMs?: number;
|
|
155
|
+
/** Cancels the restart. The server is left stopped, like any other failed restart. */
|
|
156
|
+
signal?: AbortSignal;
|
|
157
|
+
}
|
|
158
|
+
|
|
95
159
|
export interface CpaServer {
|
|
96
160
|
/** e.g. `http://127.0.0.1:8317`. Use as the client `baseUrl`. */
|
|
97
161
|
readonly origin: string;
|
|
@@ -103,13 +167,25 @@ export interface CpaServer {
|
|
|
103
167
|
readonly binaryPath: string;
|
|
104
168
|
readonly configPath: string;
|
|
105
169
|
readonly stateDir: string;
|
|
106
|
-
/** The management key given in the options, if any. */
|
|
107
|
-
readonly managementKey: string | undefined;
|
|
108
170
|
readonly process: ChildProcess;
|
|
109
171
|
/** Recent output lines (redacted), oldest first. */
|
|
110
172
|
readonly recentLogs: readonly LogLine[];
|
|
111
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Settles when the process exits for good: on `stop()` or an unexpected exit.
|
|
175
|
+
* A restart stops one process and starts another, so it does not settle this.
|
|
176
|
+
*/
|
|
112
177
|
readonly exited: Promise<ExitResult>;
|
|
113
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Stops CPA. On POSIX it sends SIGTERM and then SIGKILL after the grace
|
|
180
|
+
* period. On Windows Node cannot deliver a graceful signal, so the process is
|
|
181
|
+
* terminated forcefully (there is no grace period and CPA is not asked to
|
|
182
|
+
* flush state or update auth files). Idempotent.
|
|
183
|
+
*/
|
|
114
184
|
stop(options?: StopOptions): Promise<ExitResult>;
|
|
185
|
+
/**
|
|
186
|
+
* Restarts CPA with the original options, reusing `origin`, `baseUrl`, `host`,
|
|
187
|
+
* `port`, `stateDir` and the existing config file. Concurrent calls share one
|
|
188
|
+
* operation. A failed or cancelled restart leaves the server stopped.
|
|
189
|
+
*/
|
|
190
|
+
restart(options?: RestartOptions): Promise<void>;
|
|
115
191
|
}
|