terminay 4.2.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/dist/account.d.ts +31 -0
- package/dist/account.js +150 -0
- package/dist/account.js.map +1 -0
- package/dist/address.d.ts +14 -0
- package/dist/address.js +58 -0
- package/dist/address.js.map +1 -0
- package/dist/args.d.ts +37 -0
- package/dist/args.js +252 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +92 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/install.d.ts +48 -0
- package/dist/commands/install.js +213 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/lifecycle.d.ts +23 -0
- package/dist/commands/lifecycle.js +66 -0
- package/dist/commands/lifecycle.js.map +1 -0
- package/dist/commands/pairing.d.ts +23 -0
- package/dist/commands/pairing.js +143 -0
- package/dist/commands/pairing.js.map +1 -0
- package/dist/commands/uninstall.d.ts +26 -0
- package/dist/commands/uninstall.js +70 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/upgrade.d.ts +30 -0
- package/dist/commands/upgrade.js +175 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/context.d.ts +26 -0
- package/dist/context.js +37 -0
- package/dist/context.js.map +1 -0
- package/dist/download.d.ts +11 -0
- package/dist/download.js +95 -0
- package/dist/download.js.map +1 -0
- package/dist/health.d.ts +18 -0
- package/dist/health.js +61 -0
- package/dist/health.js.map +1 -0
- package/dist/http.d.ts +11 -0
- package/dist/http.js +88 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +30 -0
- package/dist/install.js +100 -0
- package/dist/install.js.map +1 -0
- package/dist/layout.d.ts +52 -0
- package/dist/layout.js +96 -0
- package/dist/layout.js.map +1 -0
- package/dist/manifest.d.ts +46 -0
- package/dist/manifest.js +115 -0
- package/dist/manifest.js.map +1 -0
- package/dist/platform.d.ts +17 -0
- package/dist/platform.js +34 -0
- package/dist/platform.js.map +1 -0
- package/dist/prompt.d.ts +20 -0
- package/dist/prompt.js +47 -0
- package/dist/prompt.js.map +1 -0
- package/dist/resolve.d.ts +35 -0
- package/dist/resolve.js +194 -0
- package/dist/resolve.js.map +1 -0
- package/dist/socket.d.ts +55 -0
- package/dist/socket.js +98 -0
- package/dist/socket.js.map +1 -0
- package/dist/socketClient.d.ts +1 -0
- package/dist/socketClient.js +22 -0
- package/dist/socketClient.js.map +1 -0
- package/dist/source.d.ts +24 -0
- package/dist/source.js +152 -0
- package/dist/source.js.map +1 -0
- package/dist/systemd.d.ts +30 -0
- package/dist/systemd.js +86 -0
- package/dist/systemd.js.map +1 -0
- package/dist/unit.d.ts +37 -0
- package/dist/unit.js +74 -0
- package/dist/unit.js.map +1 -0
- package/dist/verify.d.ts +24 -0
- package/dist/verify.js +46 -0
- package/dist/verify.js.map +1 -0
- package/package.json +49 -0
package/dist/source.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { cp, mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { downloadAsset } from './download.js';
|
|
7
|
+
import { hostArchitecture } from './platform.js';
|
|
8
|
+
import { DEFAULT_REPOSITORY } from './resolve.js';
|
|
9
|
+
/**
|
|
10
|
+
* Building a server archive on the target when the reference is a branch or a
|
|
11
|
+
* commit rather than a release.
|
|
12
|
+
*
|
|
13
|
+
* There is no publisher for such a build, so there is no signature to check —
|
|
14
|
+
* the operator asked for this exact commit and the machine built it locally,
|
|
15
|
+
* which is a different trust story from a downloaded artifact rather than a
|
|
16
|
+
* weaker one. The manifest is still verified when it is installed, so a build
|
|
17
|
+
* that produced a broken tree is caught before it is activated.
|
|
18
|
+
*
|
|
19
|
+
* The toolchain is checked before anything is downloaded, because finding out
|
|
20
|
+
* a compiler is missing after a shallow clone and a Node download wastes the
|
|
21
|
+
* operator's time and bandwidth.
|
|
22
|
+
*/
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
export const REQUIRED_TOOLS = [
|
|
25
|
+
'git',
|
|
26
|
+
'python3',
|
|
27
|
+
'make',
|
|
28
|
+
'c++',
|
|
29
|
+
];
|
|
30
|
+
const BUILD_TIMEOUT_MS = 45 * 60 * 1000;
|
|
31
|
+
export class ToolchainError extends Error {
|
|
32
|
+
}
|
|
33
|
+
export class SourceBuildError extends Error {
|
|
34
|
+
}
|
|
35
|
+
async function which(tool, env) {
|
|
36
|
+
try {
|
|
37
|
+
await execFileAsync('command', ['-v', tool], {
|
|
38
|
+
timeout: 30_000,
|
|
39
|
+
shell: '/bin/sh',
|
|
40
|
+
...(env === undefined ? {} : { env }),
|
|
41
|
+
});
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function preflightToolchain(tools = REQUIRED_TOOLS, env) {
|
|
49
|
+
const missing = [];
|
|
50
|
+
for (const tool of tools) {
|
|
51
|
+
if (!(await which(tool, env)))
|
|
52
|
+
missing.push(tool);
|
|
53
|
+
}
|
|
54
|
+
if (missing.length > 0) {
|
|
55
|
+
throw new ToolchainError(`building from source needs ${missing.join(', ')}, which ${missing.length === 1 ? 'is' : 'are'} not installed. Install ${missing.length === 1 ? 'it' : 'them'} (on Debian: apt install build-essential python3 git) or install a released version instead.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function run(command, args, cwd, env) {
|
|
59
|
+
const { stdout } = await execFileAsync(command, [...args], {
|
|
60
|
+
cwd,
|
|
61
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
62
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
63
|
+
...(env === undefined ? {} : { env }),
|
|
64
|
+
});
|
|
65
|
+
return stdout;
|
|
66
|
+
}
|
|
67
|
+
export async function buildFromSource(options) {
|
|
68
|
+
const write = options.write ?? (() => undefined);
|
|
69
|
+
const env = options.env;
|
|
70
|
+
const architecture = options.architecture ?? hostArchitecture();
|
|
71
|
+
const target = `linux-${architecture}`;
|
|
72
|
+
// Before any download: a missing compiler must not cost a clone.
|
|
73
|
+
await preflightToolchain(REQUIRED_TOOLS, env);
|
|
74
|
+
const parent = options.workingDirectory ?? tmpdir();
|
|
75
|
+
const build = await mkdtemp(join(parent, 'terminay-source-build-'));
|
|
76
|
+
let succeeded = false;
|
|
77
|
+
try {
|
|
78
|
+
const remote = `https://github.com/${options.repository ?? DEFAULT_REPOSITORY}.git`;
|
|
79
|
+
write(`Cloning ${options.ref} …`);
|
|
80
|
+
const checkout = join(build, 'source');
|
|
81
|
+
// A commit cannot be cloned by branch, so it is fetched and checked out.
|
|
82
|
+
if (options.revision !== undefined && /^[0-9a-f]{40}$/u.test(options.ref)) {
|
|
83
|
+
await run('git', ['init', checkout], build, env);
|
|
84
|
+
await run('git', ['remote', 'add', 'origin', remote], checkout, env);
|
|
85
|
+
await run('git', ['fetch', '--depth', '1', 'origin', options.ref], checkout, env);
|
|
86
|
+
await run('git', ['checkout', '--detach', 'FETCH_HEAD'], checkout, env);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
await run('git', ['clone', '--depth', '1', '--branch', options.ref, remote, checkout], build, env);
|
|
90
|
+
}
|
|
91
|
+
const revision = (await run('git', ['rev-parse', 'HEAD'], checkout, env)).trim();
|
|
92
|
+
write('Installing dependencies …');
|
|
93
|
+
await run('npm', ['ci'], checkout, env);
|
|
94
|
+
write('Compiling …');
|
|
95
|
+
await run('npm', ['run', 'build:application-graph'], checkout, env);
|
|
96
|
+
await run('npm', ['run', 'build:server-postcompile'], checkout, env);
|
|
97
|
+
await run('node', ['scripts/stage-selected-secure-werift-runtime.mjs'], checkout, env);
|
|
98
|
+
write('Fetching the pinned Node runtime …');
|
|
99
|
+
const nodeArchiveUrl = (await run('node', [
|
|
100
|
+
'-e',
|
|
101
|
+
'import("./scripts/pty-runtime-platforms.mjs").then((m) => process.stdout.write(m.getPtyRuntimePlatform(process.argv[1]).nodeArchive))',
|
|
102
|
+
target,
|
|
103
|
+
], checkout, env)).trim();
|
|
104
|
+
// The builder re-verifies this archive's pinned digest, so a wrong or
|
|
105
|
+
// tampered download fails there rather than being baked in.
|
|
106
|
+
const nodeArchive = await downloadAsset(nodeArchiveUrl, build, 'node-runtime.tar.xz');
|
|
107
|
+
write('Building the standalone archive …');
|
|
108
|
+
const output = join(build, 'artifact');
|
|
109
|
+
const built = await run('node', [
|
|
110
|
+
'scripts/build-standalone-server-artifact.mjs',
|
|
111
|
+
'--target',
|
|
112
|
+
target,
|
|
113
|
+
'--channel',
|
|
114
|
+
'source',
|
|
115
|
+
'--revision',
|
|
116
|
+
revision,
|
|
117
|
+
'--node-archive',
|
|
118
|
+
nodeArchive.path,
|
|
119
|
+
'--runtime-modules',
|
|
120
|
+
'node_modules',
|
|
121
|
+
'--webrtc-runtime',
|
|
122
|
+
'build/webrtc-runtime',
|
|
123
|
+
'--output-dir',
|
|
124
|
+
output,
|
|
125
|
+
], checkout, env);
|
|
126
|
+
const archivePath = JSON.parse(built).archivePath;
|
|
127
|
+
const produced = await readdir(output).catch(() => []);
|
|
128
|
+
if (typeof archivePath !== 'string' || produced.length === 0) {
|
|
129
|
+
throw new SourceBuildError('the source build produced no archive');
|
|
130
|
+
}
|
|
131
|
+
// Moved out of the build tree before that tree is removed, so the
|
|
132
|
+
// caller has something to install from.
|
|
133
|
+
const kept = join(options.destination ?? parent, basename(archivePath));
|
|
134
|
+
await rm(kept, { force: true });
|
|
135
|
+
await cp(archivePath, kept);
|
|
136
|
+
succeeded = true;
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
archivePath: kept,
|
|
139
|
+
revision,
|
|
140
|
+
buildDirectory: build,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw new SourceBuildError(`building ${options.ref} from source failed: ${error instanceof Error ? error.message : String(error)}. The build directory was kept at ${build}.`);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
// Kept on failure so the operator can read the compiler's output.
|
|
148
|
+
if (succeeded && options.keepOnFailure !== true)
|
|
149
|
+
await rm(build, { recursive: true, force: true }).catch(() => undefined);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { InstallScope } from './args.js';
|
|
2
|
+
export interface SystemdOptions {
|
|
3
|
+
readonly scope: InstallScope;
|
|
4
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
5
|
+
}
|
|
6
|
+
export interface CommandResult {
|
|
7
|
+
readonly code: number;
|
|
8
|
+
readonly stdout: string;
|
|
9
|
+
readonly stderr: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function createSystemd(options: SystemdOptions): {
|
|
12
|
+
daemonReload: () => Promise<CommandResult>;
|
|
13
|
+
enableNow: () => Promise<CommandResult>;
|
|
14
|
+
enable: () => Promise<CommandResult>;
|
|
15
|
+
disable: () => Promise<CommandResult>;
|
|
16
|
+
start: () => Promise<CommandResult>;
|
|
17
|
+
stop: () => Promise<CommandResult>;
|
|
18
|
+
resetFailed: () => Promise<CommandResult>;
|
|
19
|
+
isActive(): Promise<boolean>;
|
|
20
|
+
isEnabled(): Promise<boolean>;
|
|
21
|
+
state(): Promise<string>;
|
|
22
|
+
/** The last few journal lines, for a failure the operator has to read. */
|
|
23
|
+
journal(lines?: number): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* A user unit stops when its owner logs out unless lingering is on, so a
|
|
26
|
+
* user-scope server would die the moment the operator disconnected.
|
|
27
|
+
*/
|
|
28
|
+
enableLinger(user: string): Promise<CommandResult>;
|
|
29
|
+
};
|
|
30
|
+
export type Systemd = ReturnType<typeof createSystemd>;
|
package/dist/systemd.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { UNIT_NAME } from './layout.js';
|
|
4
|
+
/**
|
|
5
|
+
* A thin wrapper over `systemctl`, `loginctl`, and `journalctl`.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here reimplements systemd or talks to its bus: the CLI shells out to
|
|
8
|
+
* the tools already on the box. That is also what makes the rest of the CLI
|
|
9
|
+
* testable off a systemd host, because a stub earlier on PATH answers instead.
|
|
10
|
+
*/
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const TIMEOUT_MS = 120_000;
|
|
13
|
+
function scopeArguments(scope) {
|
|
14
|
+
return scope === 'user' ? ['--user'] : [];
|
|
15
|
+
}
|
|
16
|
+
async function run(command, args, options) {
|
|
17
|
+
try {
|
|
18
|
+
const { stdout, stderr } = await execFileAsync(command, [...args], {
|
|
19
|
+
timeout: TIMEOUT_MS,
|
|
20
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
21
|
+
...(options.env === undefined
|
|
22
|
+
? {}
|
|
23
|
+
: { env: options.env }),
|
|
24
|
+
});
|
|
25
|
+
return Object.freeze({ code: 0, stdout, stderr });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const failure = error;
|
|
29
|
+
// `systemctl is-active` exits non-zero to answer the question, so the
|
|
30
|
+
// exit status is returned rather than thrown for every caller to handle.
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
code: typeof failure.code === 'number' ? failure.code : 1,
|
|
33
|
+
stdout: failure.stdout ?? '',
|
|
34
|
+
stderr: failure.stderr ?? failure.message ?? '',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createSystemd(options) {
|
|
39
|
+
const systemctl = (args) => run('systemctl', [...scopeArguments(options.scope), ...args], options);
|
|
40
|
+
const expect = async (args) => {
|
|
41
|
+
const result = await systemctl(args);
|
|
42
|
+
if (result.code !== 0) {
|
|
43
|
+
throw new Error(`systemctl ${args.join(' ')} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
daemonReload: () => expect(['daemon-reload']),
|
|
49
|
+
enableNow: () => expect(['enable', '--now', UNIT_NAME]),
|
|
50
|
+
enable: () => expect(['enable', UNIT_NAME]),
|
|
51
|
+
disable: () => systemctl(['disable', UNIT_NAME]),
|
|
52
|
+
start: () => expect(['start', UNIT_NAME]),
|
|
53
|
+
stop: () => expect(['stop', UNIT_NAME]),
|
|
54
|
+
resetFailed: () => systemctl(['reset-failed', UNIT_NAME]),
|
|
55
|
+
async isActive() {
|
|
56
|
+
return (await systemctl(['is-active', UNIT_NAME])).code === 0;
|
|
57
|
+
},
|
|
58
|
+
async isEnabled() {
|
|
59
|
+
return (await systemctl(['is-enabled', UNIT_NAME])).code === 0;
|
|
60
|
+
},
|
|
61
|
+
async state() {
|
|
62
|
+
const result = await systemctl(['is-active', UNIT_NAME]);
|
|
63
|
+
return (result.stdout || result.stderr).trim() || 'unknown';
|
|
64
|
+
},
|
|
65
|
+
/** The last few journal lines, for a failure the operator has to read. */
|
|
66
|
+
async journal(lines = 20) {
|
|
67
|
+
const result = await run('journalctl', [
|
|
68
|
+
...scopeArguments(options.scope),
|
|
69
|
+
'-u',
|
|
70
|
+
UNIT_NAME,
|
|
71
|
+
'--no-pager',
|
|
72
|
+
'-n',
|
|
73
|
+
String(lines),
|
|
74
|
+
], options);
|
|
75
|
+
return (result.stdout || result.stderr).trim();
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* A user unit stops when its owner logs out unless lingering is on, so a
|
|
79
|
+
* user-scope server would die the moment the operator disconnected.
|
|
80
|
+
*/
|
|
81
|
+
async enableLinger(user) {
|
|
82
|
+
return run('loginctl', ['enable-linger', user], options);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=systemd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../src/systemd.ts"],"names":[],"mappings":"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"}
|
package/dist/unit.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { InstallLayout } from './layout.js';
|
|
2
|
+
/**
|
|
3
|
+
* The unit and environment file the installer writes.
|
|
4
|
+
*
|
|
5
|
+
* Both are generated from these templates on every install and upgrade, with
|
|
6
|
+
* one exception: the server id is written once and never rewritten, because it
|
|
7
|
+
* is the identity paired devices know the machine by. Rewriting it on upgrade
|
|
8
|
+
* would silently unpair every device.
|
|
9
|
+
*
|
|
10
|
+
* Neither file ever carries a vault passphrase, a device key, or pairing
|
|
11
|
+
* material. Those live in the data root, which is the trust boundary; a unit
|
|
12
|
+
* file is world-readable on most systems and an environment file is visible to
|
|
13
|
+
* anything that can read the service's configuration.
|
|
14
|
+
*/
|
|
15
|
+
export interface ServiceConfiguration {
|
|
16
|
+
readonly serverId: string;
|
|
17
|
+
readonly dataRoot: string;
|
|
18
|
+
readonly projectRoot: string;
|
|
19
|
+
readonly port: number;
|
|
20
|
+
readonly healthPort: number;
|
|
21
|
+
readonly expose: string;
|
|
22
|
+
readonly hostedDomain: string;
|
|
23
|
+
readonly directOrigin?: string;
|
|
24
|
+
readonly uiBundle: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function renderEnvironmentFile(configuration: ServiceConfiguration): string;
|
|
27
|
+
/**
|
|
28
|
+
* Parse an existing environment file so a reinstall can keep the values it is
|
|
29
|
+
* not being asked to change — the server id above all.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseEnvironmentFile(contents: string): Readonly<Record<string, string>>;
|
|
32
|
+
export interface UnitConfiguration {
|
|
33
|
+
readonly layout: InstallLayout;
|
|
34
|
+
readonly runAs: string;
|
|
35
|
+
readonly workingDirectory: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function renderUnit(configuration: UnitConfiguration): string;
|
package/dist/unit.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function renderEnvironmentFile(configuration) {
|
|
2
|
+
const lines = [
|
|
3
|
+
'# Written by `terminay daemon install`. Edit and restart the service to change it.',
|
|
4
|
+
'# No passphrase, device key, or pairing token belongs in this file.',
|
|
5
|
+
`TERMINAY_SERVER_ID=${configuration.serverId}`,
|
|
6
|
+
`TERMINAY_DATA_ROOT=${configuration.dataRoot}`,
|
|
7
|
+
`TERMINAY_PROJECT_ROOT=${configuration.projectRoot}`,
|
|
8
|
+
'TERMINAY_HTTP_HOST=0.0.0.0',
|
|
9
|
+
`TERMINAY_HTTP_PORT=${configuration.port}`,
|
|
10
|
+
// Health and readiness stay on loopback: they are an operator surface,
|
|
11
|
+
// not part of what the server exposes to devices.
|
|
12
|
+
'TERMINAY_HEALTH_HOST=127.0.0.1',
|
|
13
|
+
`TERMINAY_HEALTH_PORT=${configuration.healthPort}`,
|
|
14
|
+
`TERMINAY_EXPOSE=${configuration.expose}`,
|
|
15
|
+
`TERMINAY_HOSTED_DOMAIN=${configuration.hostedDomain}`,
|
|
16
|
+
...(configuration.directOrigin === undefined
|
|
17
|
+
? []
|
|
18
|
+
: [`TERMINAY_DIRECT_ORIGIN=${configuration.directOrigin}`]),
|
|
19
|
+
'TERMINAY_AGENT_INTEGRATION=enabled',
|
|
20
|
+
'TERMINAY_AI_PROVIDERS=disabled',
|
|
21
|
+
'TERMINAY_LOG_SINK=journal',
|
|
22
|
+
`TERMINAY_UI_BUNDLE=${configuration.uiBundle}`,
|
|
23
|
+
];
|
|
24
|
+
return `${lines.join('\n')}\n`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse an existing environment file so a reinstall can keep the values it is
|
|
28
|
+
* not being asked to change — the server id above all.
|
|
29
|
+
*/
|
|
30
|
+
export function parseEnvironmentFile(contents) {
|
|
31
|
+
const values = {};
|
|
32
|
+
for (const line of contents.split('\n')) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (trimmed.length === 0 || trimmed.startsWith('#'))
|
|
35
|
+
continue;
|
|
36
|
+
const equals = trimmed.indexOf('=');
|
|
37
|
+
if (equals <= 0)
|
|
38
|
+
continue;
|
|
39
|
+
values[trimmed.slice(0, equals)] = trimmed.slice(equals + 1);
|
|
40
|
+
}
|
|
41
|
+
return Object.freeze(values);
|
|
42
|
+
}
|
|
43
|
+
export function renderUnit(configuration) {
|
|
44
|
+
const { layout } = configuration;
|
|
45
|
+
// System scope names the account explicitly; a user unit already runs as
|
|
46
|
+
// its owner, and User=/Group= are rejected there.
|
|
47
|
+
const account = layout.scope === 'system'
|
|
48
|
+
? [`User=${configuration.runAs}`, `Group=${configuration.runAs}`]
|
|
49
|
+
: [];
|
|
50
|
+
return `[Unit]
|
|
51
|
+
Description=Terminay Server
|
|
52
|
+
Documentation=https://terminay.com/docs/installation
|
|
53
|
+
After=network-online.target
|
|
54
|
+
Wants=network-online.target
|
|
55
|
+
|
|
56
|
+
[Service]
|
|
57
|
+
Type=simple
|
|
58
|
+
${account.join('\n')}${account.length > 0 ? '\n' : ''}WorkingDirectory=${configuration.workingDirectory}
|
|
59
|
+
EnvironmentFile=${layout.environmentFile}
|
|
60
|
+
ExecStart=${layout.currentLink}/bin/terminay-server
|
|
61
|
+
Restart=on-failure
|
|
62
|
+
RestartSec=5s
|
|
63
|
+
KillSignal=SIGTERM
|
|
64
|
+
TimeoutStopSec=15s
|
|
65
|
+
NoNewPrivileges=true
|
|
66
|
+
PrivateTmp=true
|
|
67
|
+
StandardOutput=journal
|
|
68
|
+
StandardError=journal
|
|
69
|
+
|
|
70
|
+
[Install]
|
|
71
|
+
WantedBy=${layout.scope === 'system' ? 'multi-user.target' : 'default.target'}
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=unit.js.map
|
package/dist/unit.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit.js","sourceRoot":"","sources":["../src/unit.ts"],"names":[],"mappings":"AA4BA,MAAM,UAAU,qBAAqB,CACpC,aAAmC;IAEnC,MAAM,KAAK,GAAG;QACb,oFAAoF;QACpF,qEAAqE;QACrE,sBAAsB,aAAa,CAAC,QAAQ,EAAE;QAC9C,sBAAsB,aAAa,CAAC,QAAQ,EAAE;QAC9C,yBAAyB,aAAa,CAAC,WAAW,EAAE;QACpD,4BAA4B;QAC5B,sBAAsB,aAAa,CAAC,IAAI,EAAE;QAC1C,uEAAuE;QACvE,kDAAkD;QAClD,gCAAgC;QAChC,wBAAwB,aAAa,CAAC,UAAU,EAAE;QAClD,mBAAmB,aAAa,CAAC,MAAM,EAAE;QACzC,0BAA0B,aAAa,CAAC,YAAY,EAAE;QACtD,GAAG,CAAC,aAAa,CAAC,YAAY,KAAK,SAAS;YAC3C,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,CAAC,0BAA0B,aAAa,CAAC,YAAY,EAAE,CAAC,CAAC;QAC5D,oCAAoC;QACpC,gCAAgC;QAChC,2BAA2B;QAC3B,sBAAsB,aAAa,CAAC,QAAQ,EAAE;KAC9C,CAAC;IACF,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CACnC,QAAgB;IAEhB,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC9D,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,MAAM,IAAI,CAAC;YAAE,SAAS;QAC1B,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC9B,CAAC;AAQD,MAAM,UAAU,UAAU,CAAC,aAAgC;IAC1D,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC;IACjC,yEAAyE;IACzE,kDAAkD;IAClD,MAAM,OAAO,GACZ,MAAM,CAAC,KAAK,KAAK,QAAQ;QACxB,CAAC,CAAC,CAAC,QAAQ,aAAa,CAAC,KAAK,EAAE,EAAE,SAAS,aAAa,CAAC,KAAK,EAAE,CAAC;QACjE,CAAC,CAAC,EAAE,CAAC;IACP,OAAO;;;;;;;;EAQN,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,oBAAoB,aAAa,CAAC,gBAAgB;kBACrF,MAAM,CAAC,eAAe;YAC5B,MAAM,CAAC,WAAW;;;;;;;;;;;WAWnB,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,gBAAgB;CAC5E,CAAC;AACF,CAAC"}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boundary this crosses is "release pipeline → operator's machine", and
|
|
3
|
+
* the embedded key is the only thing that makes the download trustworthy. So
|
|
4
|
+
* there is no skip flag and no environment override: if verification cannot
|
|
5
|
+
* pass, the install does not happen. A release that cannot be signed is a
|
|
6
|
+
* release to block, not a check to relax.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RELEASE_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAYC52lwOqFefLXCpv9GcplfMj8/+QDjA1Rh95vEZlLcg=\n-----END PUBLIC KEY-----\n";
|
|
9
|
+
export declare class VerificationError extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** The sidecar is `<sha256> <filename>`, as `release-checksum.mjs` writes it. */
|
|
12
|
+
export declare function parseChecksumSidecar(contents: string): string;
|
|
13
|
+
export declare function releasePublicKey(pem?: string): import("crypto").KeyObject;
|
|
14
|
+
export interface VerifyArchiveInput {
|
|
15
|
+
readonly archivePath: string;
|
|
16
|
+
readonly sidecar: string;
|
|
17
|
+
readonly signature: Buffer;
|
|
18
|
+
/** Present when the archive was already hashed while downloading. */
|
|
19
|
+
readonly digest?: string;
|
|
20
|
+
readonly publicKeyPem?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function verifyArchive(input: VerifyArchiveInput): Promise<{
|
|
23
|
+
readonly sha256: string;
|
|
24
|
+
}>;
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { hashFile } from './download.js';
|
|
4
|
+
/**
|
|
5
|
+
* The boundary this crosses is "release pipeline → operator's machine", and
|
|
6
|
+
* the embedded key is the only thing that makes the download trustworthy. So
|
|
7
|
+
* there is no skip flag and no environment override: if verification cannot
|
|
8
|
+
* pass, the install does not happen. A release that cannot be signed is a
|
|
9
|
+
* release to block, not a check to relax.
|
|
10
|
+
*/
|
|
11
|
+
export const RELEASE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
12
|
+
MCowBQYDK2VwAyEAYC52lwOqFefLXCpv9GcplfMj8/+QDjA1Rh95vEZlLcg=
|
|
13
|
+
-----END PUBLIC KEY-----
|
|
14
|
+
`;
|
|
15
|
+
export class VerificationError extends Error {
|
|
16
|
+
}
|
|
17
|
+
/** The sidecar is `<sha256> <filename>`, as `release-checksum.mjs` writes it. */
|
|
18
|
+
export function parseChecksumSidecar(contents) {
|
|
19
|
+
const match = /^([0-9a-f]{64})\s/u.exec(contents.trim());
|
|
20
|
+
if (match?.[1] === undefined)
|
|
21
|
+
throw new VerificationError('the published checksum sidecar is not readable');
|
|
22
|
+
return match[1];
|
|
23
|
+
}
|
|
24
|
+
export function releasePublicKey(pem = RELEASE_PUBLIC_KEY_PEM) {
|
|
25
|
+
const key = createPublicKey(pem);
|
|
26
|
+
if (key.asymmetricKeyType !== 'ed25519')
|
|
27
|
+
throw new VerificationError('the embedded release key is not an Ed25519 key');
|
|
28
|
+
return key;
|
|
29
|
+
}
|
|
30
|
+
export async function verifyArchive(input) {
|
|
31
|
+
const expected = parseChecksumSidecar(input.sidecar);
|
|
32
|
+
const actual = input.digest ?? (await hashFile(input.archivePath));
|
|
33
|
+
if (actual !== expected) {
|
|
34
|
+
throw new VerificationError(`the downloaded archive does not match its published checksum (expected ${expected}, got ${actual})`);
|
|
35
|
+
}
|
|
36
|
+
// Signing the archive bytes, not the sidecar, is what stops an attacker who
|
|
37
|
+
// can replace both the asset and its checksum. Ed25519 signs a whole
|
|
38
|
+
// message, so the archive is read in full here, exactly as the release
|
|
39
|
+
// pipeline reads it when signing.
|
|
40
|
+
const payload = await readFile(input.archivePath);
|
|
41
|
+
if (!verify(null, payload, releasePublicKey(input.publicKeyPem), input.signature)) {
|
|
42
|
+
throw new VerificationError('the downloaded archive is not signed by the Terminay release key');
|
|
43
|
+
}
|
|
44
|
+
return Object.freeze({ sha256: actual });
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;CAGrC,CAAC;AAEF,MAAM,OAAO,iBAAkB,SAAQ,KAAK;CAAG;AAE/C,kFAAkF;AAClF,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACpD,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACzD,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS;QAC3B,MAAM,IAAI,iBAAiB,CAC1B,gDAAgD,CAChD,CAAC;IACH,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAc,sBAAsB;IACpE,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,GAAG,CAAC,iBAAiB,KAAK,SAAS;QACtC,MAAM,IAAI,iBAAiB,CAC1B,gDAAgD,CAChD,CAAC;IACH,OAAO,GAAG,CAAC;AACZ,CAAC;AAWD,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,KAAyB;IAEzB,MAAM,QAAQ,GAAG,oBAAoB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IACnE,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,iBAAiB,CAC1B,0EAA0E,QAAQ,SAAS,MAAM,GAAG,CACpG,CAAC;IACH,CAAC;IACD,4EAA4E;IAC5E,qEAAqE;IACrE,uEAAuE;IACvE,kCAAkC;IAClC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAClD,IACC,CAAC,MAAM,CACN,IAAI,EACJ,OAAO,EACP,gBAAgB,CAAC,KAAK,CAAC,YAAY,CAAC,EACpC,KAAK,CAAC,SAAS,CACf,EACA,CAAC;QACF,MAAM,IAAI,iBAAiB,CAC1B,kEAAkE,CAClE,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAC1C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "terminay",
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Installs and controls a headless Terminay Server on Linux.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/markwylde/terminay.git",
|
|
10
|
+
"directory": "apps/terminay-cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://terminay.com",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"terminay",
|
|
15
|
+
"terminal",
|
|
16
|
+
"daemon",
|
|
17
|
+
"systemd",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"bin": {
|
|
31
|
+
"terminay": "dist/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"deterministicBuild": true,
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"qrcode": "1.5.4"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
44
|
+
"test:ci": "node --test test/*.test.mjs"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"selfsigned": "^5.5.0"
|
|
48
|
+
}
|
|
49
|
+
}
|