@wpelevator/tug 0.6.0 → 0.8.0
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 +24 -22
- package/dist/agent-credential.d.ts +39 -0
- package/dist/agent-credential.js +135 -0
- package/dist/agent.d.ts +15 -9
- package/dist/agent.js +126 -104
- package/dist/backup-check.d.ts +11 -0
- package/dist/backup-check.js +10 -0
- package/dist/backup-config.d.ts +5 -0
- package/dist/backup-config.js +23 -3
- package/dist/backup-destination.d.ts +1 -1
- package/dist/backup-destination.js +1 -1
- package/dist/backup-restore.js +28 -8
- package/dist/backup-scheduler.d.ts +7 -1
- package/dist/backup-scheduler.js +21 -6
- package/dist/cli-build.js +70 -4
- package/dist/cli-config.d.ts +9 -5
- package/dist/cli-config.js +89 -34
- package/dist/cli-install.js +1 -1
- package/dist/cli-live-transport.d.ts +3 -2
- package/dist/cli-live-transport.js +9 -5
- package/dist/cli.js +276 -142
- package/dist/compose.d.ts +3 -1
- package/dist/compose.js +14 -2
- package/dist/control-credential.d.ts +12 -0
- package/dist/control-credential.js +46 -0
- package/dist/control.d.ts +18 -1
- package/dist/control.js +586 -202
- package/dist/credentials.d.ts +111 -0
- package/dist/{tokens.js → credentials.js} +70 -54
- package/dist/db.d.ts +20 -5
- package/dist/db.js +3 -3
- package/dist/diagnostics.d.ts +44 -0
- package/dist/diagnostics.js +65 -0
- package/dist/ingress.js +4 -3
- package/dist/main.js +35 -0
- package/dist/normalize.d.ts +6 -0
- package/dist/normalize.js +16 -2
- package/dist/pki.d.ts +119 -0
- package/dist/pki.js +339 -0
- package/dist/protocol.d.ts +53 -22
- package/dist/status-report.d.ts +11 -0
- package/dist/status-report.js +147 -0
- package/dist/templates/agent.env +1 -3
- package/dist/templates/control-compose.yml +4 -3
- package/dist/templates/control-install.sh +8 -2
- package/dist/tls-server.d.ts +11 -0
- package/dist/tls-server.js +23 -0
- package/package.json +10 -8
- package/dist/tokens.d.ts +0 -91
package/dist/cli-build.js
CHANGED
|
@@ -21,11 +21,78 @@ export function sourceArchiveFilter(entry, configPaths, ignores) {
|
|
|
21
21
|
.replace(/^(\.\/)+/, '')
|
|
22
22
|
.replace(/\/$/, '');
|
|
23
23
|
const parts = relative.split('/');
|
|
24
|
-
if (parts.some((part) => ALWAYS_EXCLUDED.includes(part)))
|
|
24
|
+
if (parts.some((part) => ALWAYS_EXCLUDED.includes(part) || CREDENTIAL_FILE.test(part)))
|
|
25
25
|
return false;
|
|
26
26
|
if (parts.length === 1 && CONFIG_NAMES.includes(relative))
|
|
27
27
|
return false;
|
|
28
|
-
|
|
28
|
+
if (configPaths.some((file) => relative === file || relative.startsWith(`${file}/`)))
|
|
29
|
+
return false;
|
|
30
|
+
return !compileIgnore(ignores)(relative);
|
|
31
|
+
}
|
|
32
|
+
/** Credential documents written by `control-install --credential-out` (default `tug-root.credential.json`). */
|
|
33
|
+
const CREDENTIAL_FILE = /\.credential\.json$/;
|
|
34
|
+
const ignoreCache = new WeakMap();
|
|
35
|
+
/** Translates one `.dockerignore` pattern: `*` and `?` stay within a path segment, `**` crosses them. */
|
|
36
|
+
function ignorePattern(pattern) {
|
|
37
|
+
let source = '';
|
|
38
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
39
|
+
const char = pattern[i];
|
|
40
|
+
if (char === '*' && pattern[i + 1] === '*') {
|
|
41
|
+
if (pattern[i + 2] === '/') {
|
|
42
|
+
source += '(?:.*/)?';
|
|
43
|
+
i += 2;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
source += '.*';
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (char === '*')
|
|
51
|
+
source += '[^/]*';
|
|
52
|
+
else if (char === '?')
|
|
53
|
+
source += '[^/]';
|
|
54
|
+
else if (char === '\\' && i + 1 < pattern.length)
|
|
55
|
+
source += pattern[++i].replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
56
|
+
else
|
|
57
|
+
source += char.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
58
|
+
}
|
|
59
|
+
return new RegExp(`^${source}$`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compiles `.dockerignore` lines with Docker's semantics: patterns are rooted at the context, a pattern that
|
|
63
|
+
* matches a directory excludes everything below it, and the last matching rule wins, so `!` re-includes.
|
|
64
|
+
*/
|
|
65
|
+
function compileIgnore(lines) {
|
|
66
|
+
const cached = ignoreCache.get(lines);
|
|
67
|
+
if (cached)
|
|
68
|
+
return cached;
|
|
69
|
+
const rules = lines
|
|
70
|
+
.map((line) => line.trim())
|
|
71
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
72
|
+
.map((line) => {
|
|
73
|
+
const negated = line.startsWith('!');
|
|
74
|
+
const pattern = path.posix
|
|
75
|
+
.normalize((negated ? line.slice(1) : line).trim())
|
|
76
|
+
.replace(/^\/+/, '')
|
|
77
|
+
.replace(/\/$/, '');
|
|
78
|
+
return { negated, pattern: ignorePattern(pattern), valid: pattern !== '' && pattern !== '.' };
|
|
79
|
+
})
|
|
80
|
+
.filter((rule) => rule.valid);
|
|
81
|
+
const matcher = (relative) => {
|
|
82
|
+
const segments = relative.split('/');
|
|
83
|
+
let ignored = false;
|
|
84
|
+
for (const { negated, pattern } of rules) {
|
|
85
|
+
for (let end = 1; end <= segments.length; end++) {
|
|
86
|
+
if (pattern.test(segments.slice(0, end).join('/'))) {
|
|
87
|
+
ignored = !negated;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return ignored;
|
|
93
|
+
};
|
|
94
|
+
ignoreCache.set(lines, matcher);
|
|
95
|
+
return matcher;
|
|
29
96
|
}
|
|
30
97
|
export async function createSourceArchive(contextDir, archive, configFiles) {
|
|
31
98
|
const configPaths = configFiles
|
|
@@ -35,8 +102,7 @@ export async function createSourceArchive(contextDir, archive, configFiles) {
|
|
|
35
102
|
try {
|
|
36
103
|
ignores.push(...(await fs.readFile(path.join(contextDir, '.dockerignore'), 'utf8'))
|
|
37
104
|
.split(/\r?\n/)
|
|
38
|
-
.map((line) => line.trim()
|
|
39
|
-
.filter((line) => line && !line.startsWith('#') && !line.startsWith('!')));
|
|
105
|
+
.map((line) => line.trim()));
|
|
40
106
|
}
|
|
41
107
|
catch (error) {
|
|
42
108
|
if (error.code !== 'ENOENT')
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Credential } from './pki.ts';
|
|
1
2
|
export type SettingType = 'string' | 'boolean' | 'port' | 'list' | 'path';
|
|
2
3
|
export interface Setting {
|
|
3
4
|
type: SettingType;
|
|
@@ -17,15 +18,18 @@ export interface ConnectionOptions {
|
|
|
17
18
|
config?: string;
|
|
18
19
|
controlUrl?: string;
|
|
19
20
|
project?: string;
|
|
21
|
+
credential?: string;
|
|
20
22
|
}
|
|
21
23
|
export interface ResolvedCliConfig {
|
|
22
24
|
/** The tracked config: selected, discovered, or where `init` creates it. */
|
|
23
25
|
file: string;
|
|
24
26
|
fileExists: boolean;
|
|
25
|
-
/** The untracked overlay next to `file`, the only place a
|
|
27
|
+
/** The untracked overlay next to `file`, the only place a credential path may be stored. */
|
|
26
28
|
localFile: string;
|
|
29
|
+
/** Where connections go: the credential's public URL unless overridden (an override changes the connect target only). */
|
|
27
30
|
controlUrl: string;
|
|
28
|
-
|
|
31
|
+
credential?: Credential;
|
|
32
|
+
credentialFile?: string;
|
|
29
33
|
project: string;
|
|
30
34
|
commandDefaults: SettingValues;
|
|
31
35
|
nodeDefaults: SettingValues;
|
|
@@ -50,11 +54,11 @@ interface OptionSource {
|
|
|
50
54
|
*/
|
|
51
55
|
export declare function applyConfigDefaults(command: OptionSource, values: object, settings: SettingValues): void;
|
|
52
56
|
/**
|
|
53
|
-
* `tug init`: create the tracked file when it is missing and always
|
|
54
|
-
* overlay, git-ignoring it. An existing tracked file is never rewritten, so its comments survive;
|
|
57
|
+
* `tug init`: create the tracked file when it is missing and always point the untracked
|
|
58
|
+
* overlay at the credential file, git-ignoring it. An existing tracked file is never rewritten, so its comments survive;
|
|
55
59
|
* connection flags passed to `init` are saved to the overlay instead.
|
|
56
60
|
*/
|
|
57
|
-
export declare function writeInitConfig(config: ResolvedCliConfig,
|
|
61
|
+
export declare function writeInitConfig(config: ResolvedCliConfig, credentialFile: string, explicit: ConnectionOptions): Promise<string[]>;
|
|
58
62
|
export declare function normalizeControlUrl(value: string): string;
|
|
59
63
|
export declare function integerOption(value: string, name: string, minimum?: number, maximum?: number): number;
|
|
60
64
|
export {};
|
package/dist/cli-config.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import yaml from 'js-yaml';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
|
+
import { decodeCredential } from "./pki.js";
|
|
6
|
+
import { errMessage } from "./util.js";
|
|
5
7
|
/** Defaults for options of commands that work on the project (top level of the config file). */
|
|
6
8
|
export const COMMAND_SETTINGS = {
|
|
7
9
|
file: { type: 'path', flag: '--file', commands: ['deploy'] },
|
|
@@ -10,7 +12,11 @@ export const COMMAND_SETTINGS = {
|
|
|
10
12
|
};
|
|
11
13
|
/** Per-node connection data (`nodes.<name>`); `nodeDefaults` takes the subset that is not node-specific. */
|
|
12
14
|
export const NODE_SETTINGS = {
|
|
13
|
-
ssh: {
|
|
15
|
+
ssh: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
flag: '<ssh-target>',
|
|
18
|
+
commands: ['node-install', 'node-replace-identity', 'node-repair-identity'],
|
|
19
|
+
},
|
|
14
20
|
address: { type: 'string', flag: '--address', commands: ['node-install', 'node-register'] },
|
|
15
21
|
publicAddress: {
|
|
16
22
|
type: 'string',
|
|
@@ -20,27 +26,53 @@ export const NODE_SETTINGS = {
|
|
|
20
26
|
label: {
|
|
21
27
|
type: 'list',
|
|
22
28
|
flag: '--label',
|
|
23
|
-
commands: ['node-install', 'node-register', 'node-
|
|
29
|
+
commands: ['node-install', 'node-register', 'node-prepare-enrollment'],
|
|
30
|
+
},
|
|
31
|
+
mesh: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
flag: '--mesh',
|
|
34
|
+
commands: [
|
|
35
|
+
'node-install',
|
|
36
|
+
'node-replace-identity',
|
|
37
|
+
'node-repair-identity',
|
|
38
|
+
'node-prepare-enrollment',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
wgPort: {
|
|
42
|
+
type: 'port',
|
|
43
|
+
flag: '--wg-port',
|
|
44
|
+
commands: ['node-install', 'node-replace-identity', 'node-repair-identity'],
|
|
24
45
|
},
|
|
25
|
-
mesh: { type: 'boolean', flag: '--mesh', commands: ['node-install', 'node-enroll'] },
|
|
26
|
-
wgPort: { type: 'port', flag: '--wg-port', commands: ['node-install'] },
|
|
27
46
|
installDocker: {
|
|
28
47
|
type: 'boolean',
|
|
29
48
|
flag: '--install-docker',
|
|
30
|
-
commands: [
|
|
49
|
+
commands: [
|
|
50
|
+
'node-install',
|
|
51
|
+
'node-replace-identity',
|
|
52
|
+
'node-repair-identity',
|
|
53
|
+
'node-prepare-enrollment',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
image: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
flag: '--image',
|
|
59
|
+
commands: [
|
|
60
|
+
'node-install',
|
|
61
|
+
'node-replace-identity',
|
|
62
|
+
'node-repair-identity',
|
|
63
|
+
'node-prepare-enrollment',
|
|
64
|
+
],
|
|
31
65
|
},
|
|
32
|
-
image: { type: 'string', flag: '--image', commands: ['node-install', 'node-enroll'] },
|
|
33
66
|
agentControlUrl: {
|
|
34
67
|
type: 'string',
|
|
35
68
|
flag: '--agent-control-url',
|
|
36
|
-
commands: [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
69
|
+
commands: [
|
|
70
|
+
'node-install',
|
|
71
|
+
'node-replace-identity',
|
|
72
|
+
'node-repair-identity',
|
|
73
|
+
'node-prepare-enrollment',
|
|
74
|
+
],
|
|
42
75
|
},
|
|
43
|
-
ca: { type: 'path', flag: '--ca', commands: ['node-enroll'] },
|
|
44
76
|
};
|
|
45
77
|
const NODE_SPECIFIC_KEYS = ['ssh', 'address', 'publicAddress'];
|
|
46
78
|
const SETTINGS = { ...COMMAND_SETTINGS, ...NODE_SETTINGS };
|
|
@@ -53,7 +85,7 @@ export function localConfigPath(file) {
|
|
|
53
85
|
}
|
|
54
86
|
/** Resolve adapter settings once; domain operations receive these explicit values. */
|
|
55
87
|
export async function resolveCliConfig(options, env, cwd, creating = false) {
|
|
56
|
-
for (const key of ['config', 'controlUrl', 'project'])
|
|
88
|
+
for (const key of ['config', 'controlUrl', 'project', 'credential'])
|
|
57
89
|
if (options[key] !== undefined && (!options[key].trim() || /[\r\n\0]/.test(options[key])))
|
|
58
90
|
throw new Error(`--${key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())} must be a nonempty single-line value`);
|
|
59
91
|
const selected = options.config || env.TUG_CONFIG;
|
|
@@ -64,13 +96,40 @@ export async function resolveCliConfig(options, env, cwd, creating = false) {
|
|
|
64
96
|
throw new Error(`Cannot read config ${file}: ENOENT: no such file`);
|
|
65
97
|
const overlay = await readConfigFile(localFile, true);
|
|
66
98
|
const config = mergeConfigs(base ?? emptyConfig(), overlay ?? emptyConfig());
|
|
67
|
-
|
|
99
|
+
if (env.TUG_CREDENTIAL && env.TUG_CREDENTIAL_FILE)
|
|
100
|
+
throw new Error('set only one of TUG_CREDENTIAL and TUG_CREDENTIAL_FILE');
|
|
101
|
+
// Precedence: --credential, then the environment, then the untracked overlay's credential path.
|
|
102
|
+
let credentialFile;
|
|
103
|
+
let credentialText;
|
|
104
|
+
if (options.credential)
|
|
105
|
+
credentialFile = path.resolve(cwd, options.credential);
|
|
106
|
+
else if (env.TUG_CREDENTIAL)
|
|
107
|
+
credentialText = env.TUG_CREDENTIAL;
|
|
108
|
+
else if (env.TUG_CREDENTIAL_FILE)
|
|
109
|
+
credentialFile = path.resolve(cwd, env.TUG_CREDENTIAL_FILE);
|
|
110
|
+
else if (config.credential)
|
|
111
|
+
credentialFile = config.credential;
|
|
112
|
+
let credential;
|
|
113
|
+
if (credentialFile || credentialText)
|
|
114
|
+
try {
|
|
115
|
+
credential = decodeCredential(credentialText ?? (await fs.readFile(credentialFile, 'utf8')));
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (!creating)
|
|
119
|
+
throw new Error(`cannot use credential ${credentialFile ?? 'from TUG_CREDENTIAL'}: ${errMessage(e)}`, { cause: e });
|
|
120
|
+
}
|
|
121
|
+
const controlUrl = options.controlUrl ||
|
|
122
|
+
env.TUG_CONTROL_URL ||
|
|
123
|
+
config.controlUrl ||
|
|
124
|
+
credential?.url ||
|
|
125
|
+
'https://localhost:8443';
|
|
68
126
|
return {
|
|
69
127
|
file,
|
|
70
128
|
fileExists: Boolean(base),
|
|
71
129
|
localFile,
|
|
72
130
|
controlUrl: normalizeControlUrl(controlUrl),
|
|
73
|
-
|
|
131
|
+
credential,
|
|
132
|
+
credentialFile,
|
|
74
133
|
project: options.project || env.TUG_PROJECT || config.project || 'site',
|
|
75
134
|
commandDefaults: config.commandDefaults,
|
|
76
135
|
nodeDefaults: config.nodeDefaults,
|
|
@@ -181,10 +240,10 @@ function parseConfig(input, dir, overlay) {
|
|
|
181
240
|
for (const [key, value] of Object.entries(input)) {
|
|
182
241
|
if (key === 'controlUrl' || key === 'project')
|
|
183
242
|
config[key] = text(value, key);
|
|
184
|
-
else if (key === '
|
|
243
|
+
else if (key === 'credential') {
|
|
185
244
|
if (overlay)
|
|
186
|
-
throw new Error(`
|
|
187
|
-
config.
|
|
245
|
+
throw new Error(`credential must not be set in a tracked file; put it in ${overlay}, or use TUG_CREDENTIAL_FILE`);
|
|
246
|
+
config.credential = path.resolve(dir, text(value, key));
|
|
188
247
|
}
|
|
189
248
|
else if (COMMAND_SETTINGS[key])
|
|
190
249
|
config.commandDefaults[key] = settingValue(COMMAND_SETTINGS[key], value, key, dir);
|
|
@@ -200,7 +259,7 @@ function parseConfig(input, dir, overlay) {
|
|
|
200
259
|
const known = [
|
|
201
260
|
'controlUrl',
|
|
202
261
|
'project',
|
|
203
|
-
...(overlay ? [] : ['
|
|
262
|
+
...(overlay ? [] : ['credential']),
|
|
204
263
|
...Object.keys(COMMAND_SETTINGS),
|
|
205
264
|
'nodeDefaults',
|
|
206
265
|
'nodes',
|
|
@@ -218,7 +277,7 @@ function mergeConfigs(base, overlay) {
|
|
|
218
277
|
return {
|
|
219
278
|
controlUrl: overlay.controlUrl ?? base.controlUrl,
|
|
220
279
|
project: overlay.project ?? base.project,
|
|
221
|
-
|
|
280
|
+
credential: overlay.credential ?? base.credential,
|
|
222
281
|
commandDefaults: { ...base.commandDefaults, ...overlay.commandDefaults },
|
|
223
282
|
nodeDefaults: { ...base.nodeDefaults, ...overlay.nodeDefaults },
|
|
224
283
|
nodes,
|
|
@@ -230,15 +289,15 @@ function serialize(file, data, notice) {
|
|
|
230
289
|
return `${notice ? `# ${notice}\n` : ''}${yaml.dump(data)}`;
|
|
231
290
|
}
|
|
232
291
|
/**
|
|
233
|
-
* `tug init`: create the tracked file when it is missing and always
|
|
234
|
-
* overlay, git-ignoring it. An existing tracked file is never rewritten, so its comments survive;
|
|
292
|
+
* `tug init`: create the tracked file when it is missing and always point the untracked
|
|
293
|
+
* overlay at the credential file, git-ignoring it. An existing tracked file is never rewritten, so its comments survive;
|
|
235
294
|
* connection flags passed to `init` are saved to the overlay instead.
|
|
236
295
|
*/
|
|
237
|
-
export async function writeInitConfig(config,
|
|
296
|
+
export async function writeInitConfig(config, credentialFile, explicit) {
|
|
238
297
|
const written = [];
|
|
239
298
|
if (!config.fileExists) {
|
|
240
299
|
await fs.mkdir(path.dirname(config.file), { recursive: true });
|
|
241
|
-
await fs.writeFile(config.file, serialize(config.file, { controlUrl: config.controlUrl, project: config.project }, `Safe to commit: keep
|
|
300
|
+
await fs.writeFile(config.file, serialize(config.file, { controlUrl: config.controlUrl, project: config.project }, `Safe to commit: keep credentials out of this file (use TUG_CREDENTIAL_FILE or ${path.basename(config.localFile)}).`));
|
|
242
301
|
written.push(config.file);
|
|
243
302
|
}
|
|
244
303
|
let local = {};
|
|
@@ -251,7 +310,7 @@ export async function writeInitConfig(config, token, explicit) {
|
|
|
251
310
|
if (error.code !== 'ENOENT')
|
|
252
311
|
throw error;
|
|
253
312
|
}
|
|
254
|
-
local.
|
|
313
|
+
local.credential = credentialFile;
|
|
255
314
|
if (config.fileExists) {
|
|
256
315
|
if (explicit.controlUrl)
|
|
257
316
|
local.controlUrl = config.controlUrl;
|
|
@@ -289,14 +348,10 @@ export function normalizeControlUrl(value) {
|
|
|
289
348
|
url = new URL(value);
|
|
290
349
|
}
|
|
291
350
|
catch {
|
|
292
|
-
throw new Error('control URL must be an absolute
|
|
351
|
+
throw new Error('control URL must be an absolute https:// URL');
|
|
293
352
|
}
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
url.password ||
|
|
297
|
-
url.search ||
|
|
298
|
-
url.hash)
|
|
299
|
-
throw new Error('control URL must use http:// or https:// without credentials, query or fragment');
|
|
353
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash)
|
|
354
|
+
throw new Error('control URL must use https:// without credentials, query or fragment');
|
|
300
355
|
return value.replace(/\/+$/, '');
|
|
301
356
|
}
|
|
302
357
|
export function integerOption(value, name, minimum = 1, maximum = Number.MAX_SAFE_INTEGER) {
|
package/dist/cli-install.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import yaml from 'js-yaml';
|
|
1
|
+
import * as yaml from 'js-yaml';
|
|
2
2
|
/** The agent dials container IPs for health checks; from the default bridge they are unreachable on current Docker (29.x), so it always shares the host network namespace. */
|
|
3
3
|
export const HOST_NETWORK_FLAG = '--network host';
|
|
4
4
|
export const MESH_RUN_FLAGS = `${HOST_NETWORK_FLAG} --cap-add NET_ADMIN`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { CliLiveTransport } from './cli-live.ts';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { Credential } from './pki.ts';
|
|
3
|
+
/** The client certificate authenticates the upgrade; `controlUrl` only picks the connect target, the credential's public URL is verified. */
|
|
4
|
+
export declare function createCliLiveTransport(credential: Credential | undefined, controlUrl: string): CliLiveTransport;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { CliLiveError } from "./cli-live.js";
|
|
3
|
+
import { clientTlsOptions } from "./pki.js";
|
|
3
4
|
import { LIVE_MAX_FRAME_BYTES as MAX_FRAME, LIVE_MAX_BUFFERED_BYTES as MAX_BUFFERED, LIVE_START_TIMEOUT_MS, } from "./protocol.js";
|
|
4
5
|
const MAX_EVENTS = 1024;
|
|
5
|
-
/**
|
|
6
|
-
export function createCliLiveTransport(
|
|
6
|
+
/** The client certificate authenticates the upgrade; `controlUrl` only picks the connect target, the credential's public URL is verified. */
|
|
7
|
+
export function createCliLiveTransport(credential, controlUrl) {
|
|
8
|
+
if (!credential)
|
|
9
|
+
throw new CliLiveError('no credential: run `tug init <credential-file>`');
|
|
7
10
|
const url = new URL(controlUrl);
|
|
8
|
-
if (url.protocol !== '
|
|
9
|
-
throw new CliLiveError('live control URL must use
|
|
11
|
+
if (url.protocol !== 'https:')
|
|
12
|
+
throw new CliLiveError('live control URL must use https');
|
|
10
13
|
if (url.username || url.password || url.search || url.hash)
|
|
11
14
|
throw new CliLiveError('live control URL must not contain credentials, query or fragment');
|
|
12
15
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
@@ -19,7 +22,8 @@ export function createCliLiveTransport(controlUrl, token) {
|
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
21
24
|
const socket = new WebSocket(url, {
|
|
22
|
-
|
|
25
|
+
...clientTlsOptions(credential),
|
|
26
|
+
followRedirects: false,
|
|
23
27
|
maxPayload: MAX_FRAME,
|
|
24
28
|
handshakeTimeout: LIVE_START_TIMEOUT_MS,
|
|
25
29
|
perMessageDeflate: false,
|