@ours.network/install 1.2.1-nightly.1 → 1.2.1-nightly.3
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 +94 -126
- package/assets/Dockerfile +1 -0
- package/assets/docker-compose.yaml +20 -0
- package/assets/release-lock.json +6 -6
- package/assets/release.json +1 -1
- package/assets/scripts/runtime/legacy-import.mjs +103 -0
- package/assets/sources.json +1 -1
- package/install.mjs +2 -2
- package/lib/build-transition.mjs +13 -7
- package/lib/effects.mjs +99 -30
- package/lib/fleet-settings.mjs +43 -0
- package/lib/legacy-migration.mjs +202 -0
- package/lib/legacy-state.mjs +205 -0
- package/lib/managed-cli.mjs +164 -0
- package/lib/orchestrate.mjs +69 -24
- package/lib/prompt.mjs +113 -119
- package/lib/server-onboarding.mjs +114 -0
- package/lib/setup-options.mjs +253 -0
- package/lib/setup.mjs +155 -0
- package/lib/usage.mjs +49 -44
- package/package.json +1 -1
package/lib/prompt.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Interactive prompts drawn on the controlling terminal (/dev/tty), so they work under
|
|
2
2
|
// `curl | bash` (where stdin/stdout are the pipe). Synchronous, dependency-free: line prompts via
|
|
3
|
-
// fs.readSync, and
|
|
3
|
+
// fs.readSync, and raw-mode single/multiple choices. When there is no tty or OURS_ASSUME_YES is
|
|
4
4
|
// set, every prompt returns its default without reading — the headless/CI path never blocks.
|
|
5
5
|
import { readSync } from 'node:fs';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
@@ -30,77 +30,66 @@ function readByte(fd) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export function askLine(write, fd, prompt, def = '') {
|
|
33
|
+
// Decode incrementally: a terminal read may split a UTF-8 code point across bytes.
|
|
34
|
+
// Fatal decoding rejects malformed input instead of storing a corrupted name or path.
|
|
35
|
+
function textEntry(write, fd, prompt, def, secret, {
|
|
36
|
+
read = readByte, enterRaw = enterSelectionMode,
|
|
37
|
+
} = {}) {
|
|
39
38
|
if (fd == null || ASSUME_YES()) return def;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let
|
|
39
|
+
let restore;
|
|
40
|
+
try { restore = enterRaw(fd); } catch (error) {
|
|
41
|
+
// A secret must never fall back to an echoing cooked terminal.
|
|
42
|
+
if (secret && isCancel(error)) { write(`${prompt}\n`); return null; }
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
let decoder = new TextDecoder('utf-8', { fatal: true });
|
|
46
|
+
let pending = false;
|
|
47
|
+
const characters = [];
|
|
48
48
|
try {
|
|
49
|
+
write(prompt);
|
|
49
50
|
for (;;) {
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
const byte = read(fd);
|
|
52
|
+
if (byte == null || byte === 0x03 || byte === 0x04) throw new InstallCancelled();
|
|
53
|
+
if (byte === 0x7f || byte === 0x08) {
|
|
54
|
+
if (pending) {
|
|
55
|
+
decoder = new TextDecoder('utf-8', { fatal: true });
|
|
56
|
+
pending = false;
|
|
57
|
+
} else if (characters.length) {
|
|
58
|
+
characters.pop();
|
|
59
|
+
if (!secret) write('\b \b');
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (byte === 0x0a || byte === 0x0d) {
|
|
64
|
+
try { decoder.decode(); } catch { throw new InstallCancelled(); }
|
|
65
|
+
write('\n');
|
|
66
|
+
const answer = characters.join('').trim();
|
|
67
|
+
return answer === '' ? def : answer;
|
|
68
|
+
}
|
|
69
|
+
if (byte < 0x20) {
|
|
70
|
+
if (pending) throw new InstallCancelled();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let decoded;
|
|
74
|
+
try { decoded = decoder.decode(Uint8Array.of(byte), { stream: true }); }
|
|
75
|
+
catch { throw new InstallCancelled(); }
|
|
76
|
+
pending = decoded.length === 0;
|
|
77
|
+
for (const character of decoded) {
|
|
78
|
+
characters.push(character);
|
|
79
|
+
if (!secret) write(character);
|
|
80
|
+
}
|
|
58
81
|
}
|
|
59
82
|
} finally {
|
|
60
83
|
restore();
|
|
61
84
|
}
|
|
62
|
-
const ans = s.trim();
|
|
63
|
-
return ans === '' ? def : ans;
|
|
64
85
|
}
|
|
65
86
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
export function askSecret(write, fd, prompt, def = '') {
|
|
70
|
-
if (fd == null || ASSUME_YES()) return def;
|
|
71
|
-
|
|
72
|
-
const saved = spawnSync('stty', ['-g'], { stdio: [fd, 'pipe', 'ignore'], encoding: 'utf8' });
|
|
73
|
-
const rawOk = saved.status === 0
|
|
74
|
-
&& spawnSync('stty', ['-icanon', '-echo', '-isig', 'min', '1', 'time', '0'], { stdio: [fd, 'ignore', 'ignore'] }).status === 0;
|
|
75
|
-
const restore = () => {
|
|
76
|
-
if (rawOk) spawnSync('stty', (saved.stdout || '').trim() ? [(saved.stdout || '').trim()] : ['sane'], { stdio: [fd, 'ignore', 'ignore'] });
|
|
77
|
-
};
|
|
78
|
-
if (!rawOk) {
|
|
79
|
-
// Fail closed: a cooked fallback would echo the secret. Returning null lets the caller
|
|
80
|
-
// explain that secure input is unavailable without ever reading a credential.
|
|
81
|
-
write(`${prompt}\n`);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
// Disable echo BEFORE displaying the prompt. Otherwise an automated or very fast typist can
|
|
85
|
-
// submit bytes in the small prompt→stty window and have the terminal driver echo the secret.
|
|
86
|
-
write(prompt);
|
|
87
|
+
export function askLine(write, fd, prompt, def = '', controls) {
|
|
88
|
+
return textEntry(write, fd, prompt, def, false, controls);
|
|
89
|
+
}
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (;;) {
|
|
91
|
-
const b = readByte(fd);
|
|
92
|
-
if (b === null || b === 0x04) break;
|
|
93
|
-
if (b === 0x03) { restore(); write('^C'); throw new InstallCancelled(); }
|
|
94
|
-
if (b === 0x0a || b === 0x0d) { write('\n'); break; }
|
|
95
|
-
if (b === 0x7f || b === 0x08) { if (s.length) s = s.slice(0, -1); continue; }
|
|
96
|
-
if (b < 0x20) continue;
|
|
97
|
-
s += String.fromCharCode(b);
|
|
98
|
-
}
|
|
99
|
-
} finally {
|
|
100
|
-
restore();
|
|
101
|
-
}
|
|
102
|
-
const ans = s.trim();
|
|
103
|
-
return ans === '' ? def : ans;
|
|
91
|
+
export function askSecret(write, fd, prompt, def = '', controls) {
|
|
92
|
+
return textEntry(write, fd, prompt, def, true, controls);
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
// askYesNo: y/n with a default shown in caps. Returns boolean.
|
|
@@ -111,69 +100,74 @@ export function askYesNo(write, fd, prompt, def = false) {
|
|
|
111
100
|
return /^y/i.test(ans);
|
|
112
101
|
}
|
|
113
102
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
// `specs` are { name, label } (optionally { checked }). No-tty → returns pre-checked defaults.
|
|
117
|
-
export function checkboxSelect(write, fd, specs, { title } = {}) {
|
|
118
|
-
const names = specs.map((s) => s.name);
|
|
119
|
-
const labels = specs.map((s) => s.label);
|
|
120
|
-
const sel = specs.map((s) => (s.checked ? 1 : 0));
|
|
121
|
-
const n = specs.length;
|
|
122
|
-
const chosen = () => names.filter((_, i) => sel[i] === 1);
|
|
123
|
-
|
|
124
|
-
if (fd == null) return chosen();
|
|
125
|
-
|
|
126
|
-
// Enter raw mode via `stty` on the tty fd (NOT tty.ReadStream.setRawMode, which would flip the
|
|
127
|
-
// fd to non-blocking and break our fs.readSync). Save the current settings and restore them
|
|
128
|
-
// after. If stty is unavailable / this isn't a real tty, fall back to the pre-checked defaults.
|
|
103
|
+
// Keep terminal controls injectable so key sequences and cleanup can be checked without a TTY.
|
|
104
|
+
function enterSelectionMode(fd) {
|
|
129
105
|
const saved = spawnSync('stty', ['-g'], { stdio: [fd, 'pipe', 'ignore'], encoding: 'utf8' });
|
|
130
|
-
if (saved.status !== 0)
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let cur = 0;
|
|
137
|
-
let drawn = 0;
|
|
106
|
+
if (saved.status !== 0 || !saved.stdout?.trim()) throw new InstallCancelled();
|
|
107
|
+
const restore = () => spawnSync('stty', [saved.stdout.trim()], { stdio: [fd, 'ignore', 'ignore'] });
|
|
108
|
+
const raw = spawnSync('stty', ['-echo', '-icanon', '-isig', 'min', '1', 'time', '0'], { stdio: [fd, 'ignore', 'ignore'] });
|
|
109
|
+
if (raw.status !== 0) { restore(); throw new InstallCancelled(); }
|
|
110
|
+
return restore;
|
|
111
|
+
}
|
|
138
112
|
|
|
113
|
+
function choose(write, fd, question, choices, defaults, multiple, {
|
|
114
|
+
read = readByte, enterRaw = enterSelectionMode,
|
|
115
|
+
} = {}) {
|
|
116
|
+
if (!choices.length) throw new Error('A choice prompt requires at least one choice');
|
|
117
|
+
const selected = choices.map(choice => defaults.includes(choice.value));
|
|
118
|
+
let current = Math.max(0, selected.indexOf(true));
|
|
119
|
+
const result = () => multiple
|
|
120
|
+
? choices.filter((_, i) => selected[i]).map(choice => choice.value)
|
|
121
|
+
: choices[current].value;
|
|
122
|
+
if (fd == null || ASSUME_YES()) return result();
|
|
123
|
+
const restore = enterRaw(fd);
|
|
124
|
+
let drawn = false;
|
|
125
|
+
const next = () => {
|
|
126
|
+
const byte = read(fd);
|
|
127
|
+
if (byte == null || byte === 0x03 || byte === 0x04) throw new InstallCancelled();
|
|
128
|
+
return byte;
|
|
129
|
+
};
|
|
139
130
|
const redraw = () => {
|
|
140
|
-
if (drawn) write(`\x1b[${
|
|
141
|
-
drawn =
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
131
|
+
if (drawn) write(`\x1b[${choices.length}A`);
|
|
132
|
+
drawn = true;
|
|
133
|
+
choices.forEach((choice, i) => {
|
|
134
|
+
const marker = multiple ? (selected[i] ? c.green('[x]') : '[ ]') : (i === current ? c.green('(●)') : '( )');
|
|
135
|
+
write(`\r\x1b[K ${i === current ? c.cyan('> ') : ' '}${marker} ${choice.label}\n`);
|
|
136
|
+
});
|
|
147
137
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (b2 === 0x41) cur = (cur - 1 + n) % n; // up
|
|
161
|
-
else if (b2 === 0x42) cur = (cur + 1) % n; // down
|
|
138
|
+
try {
|
|
139
|
+
write(`\x1b[?25l ${question}\n ${c.bold('↑/↓')} move${multiple ? ', ' + c.bold('Space') + ' toggle' : ''}, ${c.bold('Enter')} confirm\n`);
|
|
140
|
+
redraw();
|
|
141
|
+
for (;;) {
|
|
142
|
+
const byte = next();
|
|
143
|
+
if (byte === 0x0d || byte === 0x0a) return result();
|
|
144
|
+
if (byte === 0x1b) {
|
|
145
|
+
const prefix = next();
|
|
146
|
+
if (prefix === 0x5b || prefix === 0x4f) {
|
|
147
|
+
const direction = next();
|
|
148
|
+
if (direction === 0x41) current = (current - 1 + choices.length) % choices.length;
|
|
149
|
+
if (direction === 0x42) current = (current + 1) % choices.length;
|
|
162
150
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
case 0x6b: case 0x4b: cur = (cur - 1 + n) % n; break; // k/K
|
|
166
|
-
case 0x6a: case 0x4a: cur = (cur + 1) % n; break; // j/J
|
|
167
|
-
case 0x20: sel[cur] = 1 - sel[cur]; break; // space
|
|
168
|
-
case 0x61: case 0x41: for (let i = 0; i < n; i++) sel[i] = 1; break; // a/A
|
|
169
|
-
case 0x6e: case 0x4e: for (let i = 0; i < n; i++) sel[i] = 0; break; // n/N
|
|
170
|
-
case 0x71: case 0x51: for (let i = 0; i < n; i++) sel[i] = 0; break loop; // q/Q cancel
|
|
171
|
-
case 0x0d: case 0x0a: break loop; // enter
|
|
172
|
-
default: break;
|
|
151
|
+
} else if (multiple && byte === 0x20) selected[current] = !selected[current];
|
|
152
|
+
redraw();
|
|
173
153
|
}
|
|
174
|
-
|
|
154
|
+
} finally {
|
|
155
|
+
try { restore(); } finally { write('\x1b[?25h'); }
|
|
175
156
|
}
|
|
157
|
+
}
|
|
176
158
|
|
|
177
|
-
|
|
178
|
-
return
|
|
159
|
+
export function select(write, fd, question, choices, defaultValue, controls) {
|
|
160
|
+
return choose(write, fd, question, choices, [defaultValue], false, controls);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function multiselect(write, fd, question, choices, defaults = [], controls) {
|
|
164
|
+
return choose(write, fd, question, choices, defaults, true, controls);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Preserve the existing component selection API using the same keyboard implementation.
|
|
168
|
+
export function checkboxSelect(write, fd, specs, { title } = {}) {
|
|
169
|
+
if (!specs.length) return [];
|
|
170
|
+
return multiselect(write, fd, title || 'Choose components',
|
|
171
|
+
specs.map(({ name, label }) => ({ value: name, label })),
|
|
172
|
+
specs.filter(spec => spec.checked).map(spec => spec.name));
|
|
179
173
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Human bootstrap and local client handoff through the existing owner interfaces. */
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, isAbsolute } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { installationPaths } from './plan.mjs';
|
|
6
|
+
import { validateHostProfile } from './target.mjs';
|
|
7
|
+
|
|
8
|
+
export function validateIdentityName(name) {
|
|
9
|
+
if (typeof name !== 'string' || [...name].length < 1 || [...name].length > 64 || name !== name.normalize('NFC')
|
|
10
|
+
|| /[\\/\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u.test(name)
|
|
11
|
+
|| ['.', '..', 'contact-book', 'root.json', 'bindings.json'].includes(name)) {
|
|
12
|
+
throw new Error('Invalid Human identity name; use 1–64 NFC characters without reserved names or path/control characters');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function privatePath(path, directory = false) {
|
|
16
|
+
const stat = lstatSync(path);
|
|
17
|
+
if (!(directory ? stat.isDirectory() : stat.isFile()) || realpathSync(path) !== path
|
|
18
|
+
|| stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0 || (!directory && stat.nlink !== 1)) {
|
|
19
|
+
throw new Error('Onboarding requires an owned private ' + (directory ? 'directory' : 'regular credential file'));
|
|
20
|
+
}
|
|
21
|
+
return stat;
|
|
22
|
+
}
|
|
23
|
+
function validateRecord(record) {
|
|
24
|
+
if (!record || !['packages', 'docker'].includes(record.mode) || typeof record.root !== 'string'
|
|
25
|
+
|| !isAbsolute(record.root) || resolve(record.root) !== record.root
|
|
26
|
+
|| !Number.isInteger(record.port) || record.port < 1 || record.port > 65535) throw new Error('Invalid local server selection');
|
|
27
|
+
validateHostProfile({ endpoint: `http://127.0.0.1:${record.port}`, expectedInstanceId: record.instanceId, credentialPath: join(record.root, 'client', 'credential') });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createServerOnboarding(effects, { compose, localEnv, bin }) {
|
|
31
|
+
async function identityCommand(record, args) {
|
|
32
|
+
const result = record.mode === 'docker'
|
|
33
|
+
? await compose(record, ['exec', '-T', 'daemon', 'node', '/opt/ours/node_modules/@ours.network/cli/dist/cli.js', ...args, '--config', '/var/lib/ours/config.json', '--state-dir', '/var/lib/ours', '--json'])
|
|
34
|
+
: await effects.run(bin(record, 'ours'), [...args, '--config', record.configPath, '--state-dir', installationPaths(record).daemon, '--json'], { env: localEnv(record) });
|
|
35
|
+
if (result.code !== undefined && result.code !== 0) throw new Error('Owner identity operation failed');
|
|
36
|
+
try { return JSON.parse(result.stdout); }
|
|
37
|
+
catch { throw new Error('Owner identity operation returned malformed JSON'); }
|
|
38
|
+
}
|
|
39
|
+
async function identities(record) {
|
|
40
|
+
const rows = await identityCommand(record, ['identity', 'list']);
|
|
41
|
+
if (!Array.isArray(rows) || rows.some(row => !row || typeof row.name !== 'string'
|
|
42
|
+
|| (!['root', 'role'].includes(row.kind) && !['reconciling', 'awaiting-root', 'migration-failed', 'refresh-failed'].includes(row.status))
|
|
43
|
+
|| (row.kind && (typeof row.cid !== 'string' || !row.cid)))) throw new Error('Owner identity list is malformed');
|
|
44
|
+
const roots = rows.filter(row => row.kind === 'root');
|
|
45
|
+
if (roots.length > 1) throw new Error('Owner identity list contains multiple Human roots');
|
|
46
|
+
return { rows, root: roots[0] };
|
|
47
|
+
}
|
|
48
|
+
function retained({ rows, root }) {
|
|
49
|
+
effects.out?.(`Retained ${rows.length} existing ${rows.length === 1 ? 'identity' : 'identities'}; Human identity: ${root.name}.`);
|
|
50
|
+
return { name: root.name, cid: root.cid, created: false };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async serverListIdentities(record) { return (await identities(record)).rows; },
|
|
54
|
+
async serverEnsureIdentity(record, name) {
|
|
55
|
+
validateRecord(record);
|
|
56
|
+
validateIdentityName(name);
|
|
57
|
+
const prior = await identities(record);
|
|
58
|
+
if (prior.root) return retained(prior);
|
|
59
|
+
if (prior.rows.some(row => row.name === name)) throw new Error('Requested Human identity name already exists; no identity was changed');
|
|
60
|
+
effects.out?.(`Creating Human identity ${name}; retaining ${prior.rows.length} existing identities.`);
|
|
61
|
+
try {
|
|
62
|
+
await identityCommand(record, ['identity', 'create-root', '--name', name, '--skip-if-root-exists', 'true']);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// ROOT_EXISTS is currently a CLI error. A concurrent creator is safe only
|
|
65
|
+
// when the authoritative list now contains a root; other errors stay errors.
|
|
66
|
+
const after = await identities(record);
|
|
67
|
+
if (after.root) return retained(after);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const after = await identities(record);
|
|
71
|
+
if (!after.root) throw new Error('Human identity creation completed without a visible root');
|
|
72
|
+
if (after.root.name !== name) return retained(after);
|
|
73
|
+
effects.out?.(`Human identity ${after.root.name} is ready.`);
|
|
74
|
+
return { name: after.root.name, cid: after.root.cid, created: true };
|
|
75
|
+
},
|
|
76
|
+
async prepareLocalClient(record, integrations, fleetSettingsPath) {
|
|
77
|
+
validateRecord(record);
|
|
78
|
+
if (!Array.isArray(integrations) || !integrations.length || new Set(integrations).size !== integrations.length
|
|
79
|
+
|| integrations.some(name => !['codex', 'claude-code', 'fleet'].includes(name))) throw new Error('Client integrations must select codex, claude-code and/or fleet');
|
|
80
|
+
if (fleetSettingsPath !== undefined) {
|
|
81
|
+
if (typeof fleetSettingsPath !== 'string' || !isAbsolute(fleetSettingsPath) || resolve(fleetSettingsPath) !== fleetSettingsPath) throw new Error('Fleet settings path must be absolute');
|
|
82
|
+
const settings = JSON.parse(readFileSync(fleetSettingsPath, 'utf8'));
|
|
83
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) throw new Error('Fleet settings must be a JSON object');
|
|
84
|
+
}
|
|
85
|
+
const endpoint = `http://127.0.0.1:${record.port}`;
|
|
86
|
+
const current = effects.readManagedClientProfile();
|
|
87
|
+
if (current && (current.endpoint !== endpoint || current.expectedInstanceId !== record.instanceId)) throw new Error('Managed client already selects another server; no credential was issued');
|
|
88
|
+
privatePath(record.root, true);
|
|
89
|
+
const root = join(record.root, 'client');
|
|
90
|
+
if (!existsSync(root)) mkdirSync(root, { mode: 0o700 });
|
|
91
|
+
privatePath(root, true);
|
|
92
|
+
const stage = mkdtempSync(join(root, '.pending-'));
|
|
93
|
+
const published = join(root, 'issued-' + randomUUID());
|
|
94
|
+
const credential = join(stage, 'credential');
|
|
95
|
+
try {
|
|
96
|
+
effects.out?.('Issuing a separate local client credential with the retained server authority.');
|
|
97
|
+
try { await effects.serverAccess(record, 'access-issue', { output: credential }); }
|
|
98
|
+
catch { throw new Error('Client credential issuance failed; existing profiles were retained'); }
|
|
99
|
+
const stat = privatePath(credential);
|
|
100
|
+
if (stat.size > 4096 || !readFileSync(credential, 'utf8').trim()) throw new Error('Issued client credential is empty or invalid');
|
|
101
|
+
const profile = {
|
|
102
|
+
...validateHostProfile({ endpoint, expectedInstanceId: record.instanceId, credentialPath: join(published, 'credential') }),
|
|
103
|
+
installer: { integrations: [...integrations], ...(fleetSettingsPath !== undefined ? { fleetSettingsPath } : {}) },
|
|
104
|
+
};
|
|
105
|
+
writeFileSync(join(stage, 'profile.json'), JSON.stringify(profile, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
106
|
+
// Publish the complete pair in one rename. Managed-default activation and
|
|
107
|
+
// package/source selection remain the normal client installer's job.
|
|
108
|
+
renameSync(stage, published);
|
|
109
|
+
effects.out?.('Local client profile is ready for authenticated client setup.');
|
|
110
|
+
return { configPath: join(published, 'profile.json'), profile };
|
|
111
|
+
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|