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.
Files changed (79) hide show
  1. package/dist/account.d.ts +31 -0
  2. package/dist/account.js +150 -0
  3. package/dist/account.js.map +1 -0
  4. package/dist/address.d.ts +14 -0
  5. package/dist/address.js +58 -0
  6. package/dist/address.js.map +1 -0
  7. package/dist/args.d.ts +37 -0
  8. package/dist/args.js +252 -0
  9. package/dist/args.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +92 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/commands/install.d.ts +48 -0
  14. package/dist/commands/install.js +213 -0
  15. package/dist/commands/install.js.map +1 -0
  16. package/dist/commands/lifecycle.d.ts +23 -0
  17. package/dist/commands/lifecycle.js +66 -0
  18. package/dist/commands/lifecycle.js.map +1 -0
  19. package/dist/commands/pairing.d.ts +23 -0
  20. package/dist/commands/pairing.js +143 -0
  21. package/dist/commands/pairing.js.map +1 -0
  22. package/dist/commands/uninstall.d.ts +26 -0
  23. package/dist/commands/uninstall.js +70 -0
  24. package/dist/commands/uninstall.js.map +1 -0
  25. package/dist/commands/upgrade.d.ts +30 -0
  26. package/dist/commands/upgrade.js +175 -0
  27. package/dist/commands/upgrade.js.map +1 -0
  28. package/dist/context.d.ts +26 -0
  29. package/dist/context.js +37 -0
  30. package/dist/context.js.map +1 -0
  31. package/dist/download.d.ts +11 -0
  32. package/dist/download.js +95 -0
  33. package/dist/download.js.map +1 -0
  34. package/dist/health.d.ts +18 -0
  35. package/dist/health.js +61 -0
  36. package/dist/health.js.map +1 -0
  37. package/dist/http.d.ts +11 -0
  38. package/dist/http.js +88 -0
  39. package/dist/http.js.map +1 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.js +11 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/install.d.ts +30 -0
  44. package/dist/install.js +100 -0
  45. package/dist/install.js.map +1 -0
  46. package/dist/layout.d.ts +52 -0
  47. package/dist/layout.js +96 -0
  48. package/dist/layout.js.map +1 -0
  49. package/dist/manifest.d.ts +46 -0
  50. package/dist/manifest.js +115 -0
  51. package/dist/manifest.js.map +1 -0
  52. package/dist/platform.d.ts +17 -0
  53. package/dist/platform.js +34 -0
  54. package/dist/platform.js.map +1 -0
  55. package/dist/prompt.d.ts +20 -0
  56. package/dist/prompt.js +47 -0
  57. package/dist/prompt.js.map +1 -0
  58. package/dist/resolve.d.ts +35 -0
  59. package/dist/resolve.js +194 -0
  60. package/dist/resolve.js.map +1 -0
  61. package/dist/socket.d.ts +55 -0
  62. package/dist/socket.js +98 -0
  63. package/dist/socket.js.map +1 -0
  64. package/dist/socketClient.d.ts +1 -0
  65. package/dist/socketClient.js +22 -0
  66. package/dist/socketClient.js.map +1 -0
  67. package/dist/source.d.ts +24 -0
  68. package/dist/source.js +152 -0
  69. package/dist/source.js.map +1 -0
  70. package/dist/systemd.d.ts +30 -0
  71. package/dist/systemd.js +86 -0
  72. package/dist/systemd.js.map +1 -0
  73. package/dist/unit.d.ts +37 -0
  74. package/dist/unit.js +74 -0
  75. package/dist/unit.js.map +1 -0
  76. package/dist/verify.d.ts +24 -0
  77. package/dist/verify.js +46 -0
  78. package/dist/verify.js.map +1 -0
  79. package/package.json +49 -0
package/dist/http.js ADDED
@@ -0,0 +1,88 @@
1
+ import { get } from 'node:https';
2
+ /**
3
+ * The CLI talks to exactly one host family (github.com) over TLS and reads
4
+ * bounded documents. `node:https` covers that without a client library, which
5
+ * keeps the dependency an operator runs as root down to the QR renderer.
6
+ */
7
+ const USER_AGENT = 'terminay-cli';
8
+ const MAX_REDIRECTS = 5;
9
+ const MAX_DOCUMENT_BYTES = 4 * 1024 * 1024;
10
+ const REQUEST_TIMEOUT_MS = 30_000;
11
+ /** Sentinel for `openStream`: return the redirect itself rather than follow it. */
12
+ export const NO_REDIRECT = -1;
13
+ function headers(accept) {
14
+ const value = { 'user-agent': USER_AGENT, accept };
15
+ // An authenticated request lifts the anonymous rate limit that an `npx`
16
+ // install on a busy network can otherwise hit.
17
+ const token = process.env.GITHUB_TOKEN;
18
+ if (typeof token === 'string' && token.length > 0)
19
+ value.authorization = `Bearer ${token}`;
20
+ return value;
21
+ }
22
+ export function openStream(url, accept = '*/*', redirectsLeft = MAX_REDIRECTS, extraHeaders) {
23
+ return new Promise((resolve, reject) => {
24
+ const parsed = new URL(url);
25
+ if (parsed.protocol !== 'https:') {
26
+ reject(new Error(`refusing a non-HTTPS download: ${url}`));
27
+ return;
28
+ }
29
+ const options = {
30
+ headers: { ...headers(accept), ...extraHeaders },
31
+ };
32
+ const request = get(parsed, options, (response) => {
33
+ const status = response.statusCode ?? 0;
34
+ const location = response.headers.location;
35
+ if (status >= 300 && status < 400 && typeof location === 'string') {
36
+ // A caller passing NO_REDIRECT wants the hop itself, not its target.
37
+ if (redirectsLeft === NO_REDIRECT) {
38
+ resolve(response);
39
+ return;
40
+ }
41
+ response.resume();
42
+ if (redirectsLeft <= 0) {
43
+ reject(new Error(`too many redirects fetching ${url}`));
44
+ return;
45
+ }
46
+ // A redirect to a signed asset host must not carry the GitHub
47
+ // token onward, so the next hop is a fresh request.
48
+ resolve(openStream(new URL(location, parsed).toString(), accept, redirectsLeft - 1, extraHeaders));
49
+ return;
50
+ }
51
+ resolve(response);
52
+ });
53
+ request.setTimeout(REQUEST_TIMEOUT_MS, () => request.destroy(new Error(`timed out fetching ${url}`)));
54
+ request.on('error', reject);
55
+ });
56
+ }
57
+ export async function request(url, accept = '*/*', followRedirects = true) {
58
+ // A caller that needs to read where a redirect points — `releases/latest`
59
+ // names the newest tag only in its Location — asks not to be followed.
60
+ const response = await openStream(url, accept, followRedirects ? MAX_REDIRECTS : NO_REDIRECT);
61
+ const chunks = [];
62
+ let total = 0;
63
+ for await (const chunk of response) {
64
+ total += chunk.length;
65
+ if (total > MAX_DOCUMENT_BYTES) {
66
+ response.destroy();
67
+ throw new Error(`response from ${url} is larger than the ${MAX_DOCUMENT_BYTES}-byte limit`);
68
+ }
69
+ chunks.push(chunk);
70
+ }
71
+ return Object.freeze({
72
+ status: response.statusCode ?? 0,
73
+ headers: response.headers,
74
+ body: Buffer.concat(chunks).toString('utf8'),
75
+ });
76
+ }
77
+ export async function requestJson(url) {
78
+ const response = await request(url, 'application/vnd.github+json');
79
+ if (response.status !== 200)
80
+ throw new Error(`GitHub returned ${response.status} for ${url}`);
81
+ try {
82
+ return JSON.parse(response.body);
83
+ }
84
+ catch {
85
+ throw new Error(`GitHub returned an unreadable document for ${url}`);
86
+ }
87
+ }
88
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export type { DaemonCommand, DaemonOptions, InstallScope, PairingMode, ParsedCommandLine, } from './args.js';
2
+ export { HELP_TEXT, parseCommandLine, UsageError } from './args.js';
3
+ export { runInstall } from './commands/install.js';
4
+ export { runStart, runStatus, runStop } from './commands/lifecycle.js';
5
+ export { runApprovals, runPairing, runResolveApproval, } from './commands/pairing.js';
6
+ export { runResetIdentity, runUninstall } from './commands/uninstall.js';
7
+ export { runUpgrade } from './commands/upgrade.js';
8
+ export { NotInstalledError, resolveContext } from './context.js';
9
+ export { installLayout, readInstallRecord, writeInstallRecord, } from './layout.js';
10
+ export type { HostFacts } from './platform.js';
11
+ export { assertSupportedHost, hostArchitecture, readHostFacts, SYSTEMD_MARKER, UnsupportedHostError, } from './platform.js';
12
+ export { buildFromSource, preflightToolchain } from './source.js';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { HELP_TEXT, parseCommandLine, UsageError } from './args.js';
2
+ export { runInstall } from './commands/install.js';
3
+ export { runStart, runStatus, runStop } from './commands/lifecycle.js';
4
+ export { runApprovals, runPairing, runResolveApproval, } from './commands/pairing.js';
5
+ export { runResetIdentity, runUninstall } from './commands/uninstall.js';
6
+ export { runUpgrade } from './commands/upgrade.js';
7
+ export { NotInstalledError, resolveContext } from './context.js';
8
+ export { installLayout, readInstallRecord, writeInstallRecord, } from './layout.js';
9
+ export { assertSupportedHost, hostArchitecture, readHostFacts, SYSTEMD_MARKER, UnsupportedHostError, } from './platform.js';
10
+ export { buildFromSource, preflightToolchain } from './source.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EACN,YAAY,EACZ,UAAU,EACV,kBAAkB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EACN,aAAa,EACb,iBAAiB,EACjB,kBAAkB,GAClB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,oBAAoB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { InstallLayout } from './layout.js';
2
+ import { type ArchiveManifest, type ExpectedRelease } from './manifest.js';
3
+ import type { ReleaseChannel } from './resolve.js';
4
+ export interface InstalledVersion {
5
+ readonly directory: string;
6
+ readonly name: string;
7
+ readonly manifest: ArchiveManifest;
8
+ }
9
+ /**
10
+ * The archive contains a single top-level directory. It is extracted into a
11
+ * scratch directory first so a failed or partial unpack never appears under
12
+ * `versions/`, where the next command would treat it as installed.
13
+ */
14
+ export declare function installArchive(options: {
15
+ readonly layout: InstallLayout;
16
+ readonly archivePath: string;
17
+ readonly channel: ReleaseChannel;
18
+ readonly expected?: ExpectedRelease;
19
+ }): Promise<InstalledVersion>;
20
+ /** Point `current` at a version. Written beside the link, then renamed over it. */
21
+ export declare function activate(layout: InstallLayout, versionName: string): Promise<void>;
22
+ export declare function activeVersion(layout: InstallLayout): Promise<string | undefined>;
23
+ /** Roll back to a version known to have run. Identical to activation. */
24
+ export declare function rollback(layout: InstallLayout, versionName: string): Promise<void>;
25
+ export declare function installedVersions(layout: InstallLayout): Promise<readonly string[]>;
26
+ /**
27
+ * Keep the active version and one previous, so a bad upgrade always has
28
+ * something proven to fall back to and disk use stays bounded.
29
+ */
30
+ export declare function retain(layout: InstallLayout, keep: readonly string[]): Promise<readonly string[]>;
@@ -0,0 +1,100 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { chmod, lstat, mkdir, mkdtemp, readdir, readlink, rename, rm, symlink, } from 'node:fs/promises';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { stagedName } from './layout.js';
6
+ import { validateUnpackedArchive, } from './manifest.js';
7
+ /**
8
+ * Laying a verified archive down on disk, and switching between versions
9
+ * without ever pointing `current` at something incomplete.
10
+ *
11
+ * Unpack, verify, then rename: the new version is fully present and hashed
12
+ * before anything references it, and the reference itself changes in a single
13
+ * atomic step. An interrupt at any point leaves `current` pointing at a
14
+ * complete version — either the old one or the new one, never a half-built
15
+ * directory.
16
+ */
17
+ const execFileAsync = promisify(execFile);
18
+ /**
19
+ * The archive contains a single top-level directory. It is extracted into a
20
+ * scratch directory first so a failed or partial unpack never appears under
21
+ * `versions/`, where the next command would treat it as installed.
22
+ */
23
+ export async function installArchive(options) {
24
+ const { layout, archivePath } = options;
25
+ await mkdir(layout.versionsDirectory, { recursive: true, mode: 0o755 });
26
+ const scratch = await mkdtemp(join(layout.versionsDirectory, '.staging-'));
27
+ try {
28
+ await execFileAsync('tar', ['-xzf', archivePath, '-C', scratch], {
29
+ maxBuffer: 8 * 1024 * 1024,
30
+ });
31
+ const entries = await readdir(scratch, { withFileTypes: true });
32
+ const roots = entries.filter((entry) => entry.isDirectory());
33
+ if (roots.length !== 1 || entries.length !== roots.length) {
34
+ throw new Error('the server archive does not contain exactly one top-level directory');
35
+ }
36
+ const unpacked = join(scratch, roots[0].name);
37
+ const manifest = await validateUnpackedArchive(unpacked, options.expected ?? {});
38
+ const name = stagedName(options.channel, manifest.version, manifest.revision);
39
+ const directory = join(layout.versionsDirectory, name);
40
+ // Re-installing the same version replaces it, because the payload was
41
+ // just proven to match its signed manifest byte for byte.
42
+ await rm(directory, { recursive: true, force: true });
43
+ await rename(unpacked, directory);
44
+ await chmod(directory, 0o755);
45
+ return Object.freeze({ directory, name, manifest });
46
+ }
47
+ finally {
48
+ await rm(scratch, { recursive: true, force: true });
49
+ }
50
+ }
51
+ /** Point `current` at a version. Written beside the link, then renamed over it. */
52
+ export async function activate(layout, versionName) {
53
+ const target = join(layout.versionsDirectory, versionName);
54
+ const info = await lstat(target).catch(() => undefined);
55
+ if (info === undefined || !info.isDirectory())
56
+ throw new Error(`cannot activate ${versionName}: it is not installed`);
57
+ await mkdir(dirname(layout.currentLink), { recursive: true, mode: 0o755 });
58
+ const temporary = `${layout.currentLink}.tmp`;
59
+ await rm(temporary, { force: true });
60
+ // Relative, so the tree can be relocated and inspected without the link
61
+ // dangling in a way that hides which version was active.
62
+ await symlink(join('versions', versionName), temporary);
63
+ await rename(temporary, layout.currentLink);
64
+ }
65
+ export async function activeVersion(layout) {
66
+ const target = await readlink(layout.currentLink).catch(() => undefined);
67
+ return target === undefined ? undefined : basename(target);
68
+ }
69
+ /** Roll back to a version known to have run. Identical to activation. */
70
+ export async function rollback(layout, versionName) {
71
+ await activate(layout, versionName);
72
+ }
73
+ export async function installedVersions(layout) {
74
+ const entries = await readdir(layout.versionsDirectory, {
75
+ withFileTypes: true,
76
+ }).catch(() => []);
77
+ return entries
78
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
79
+ .map((entry) => entry.name)
80
+ .sort();
81
+ }
82
+ /**
83
+ * Keep the active version and one previous, so a bad upgrade always has
84
+ * something proven to fall back to and disk use stays bounded.
85
+ */
86
+ export async function retain(layout, keep) {
87
+ const kept = new Set(keep.filter((name) => name.length > 0));
88
+ const removed = [];
89
+ for (const name of await installedVersions(layout)) {
90
+ if (kept.has(name))
91
+ continue;
92
+ await rm(join(layout.versionsDirectory, name), {
93
+ recursive: true,
94
+ force: true,
95
+ });
96
+ removed.push(name);
97
+ }
98
+ return removed;
99
+ }
100
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { InstallScope } from './args.js';
2
+ import type { ReleaseChannel } from './resolve.js';
3
+ /**
4
+ * Where an installation lives, and the one small record that describes it.
5
+ *
6
+ * Every version sits under `versions/<version>` and is immutable once
7
+ * verified; `current` is a symbolic link swapped by rename. Nothing the CLI
8
+ * does later edits a versioned directory, so an upgrade that fails can put
9
+ * the link back and be exactly where it started.
10
+ */
11
+ export declare const UNIT_NAME = "terminay-server.service";
12
+ export declare const INSTALL_RECORD = "install.json";
13
+ export interface InstallLayout {
14
+ readonly scope: InstallScope;
15
+ readonly prefix: string;
16
+ readonly versionsDirectory: string;
17
+ readonly currentLink: string;
18
+ readonly recordPath: string;
19
+ readonly environmentFile: string;
20
+ readonly defaultDataRoot: string;
21
+ readonly unitPath: string;
22
+ }
23
+ export declare function installLayout(scope: InstallScope, home?: string): InstallLayout;
24
+ /** What the CLI needs to remember between commands. It holds no secrets. */
25
+ export interface InstallRecord {
26
+ readonly schemaVersion: 1;
27
+ readonly scope: InstallScope;
28
+ readonly channel: ReleaseChannel;
29
+ readonly version: string;
30
+ /** The commit the installed archive records in its own signed manifest. */
31
+ readonly revision: string;
32
+ readonly publishedAt?: string;
33
+ readonly runAs: string;
34
+ readonly dataRoot: string;
35
+ readonly projectRoot: string;
36
+ readonly port: number;
37
+ readonly healthPort: number;
38
+ readonly expose: string;
39
+ readonly hostedDomain: string;
40
+ readonly directOrigin?: string;
41
+ readonly installedAt: string;
42
+ }
43
+ export declare function parseInstallRecord(value: unknown): InstallRecord;
44
+ export declare function readInstallRecord(layout: InstallLayout): Promise<InstallRecord | undefined>;
45
+ export declare function writeInstallRecord(layout: InstallLayout, record: InstallRecord): Promise<void>;
46
+ export declare function versionDirectory(layout: InstallLayout, version: string): string;
47
+ /**
48
+ * The rolling channel reuses the version string `main` for every build, so its
49
+ * directories are named for the commit instead. Without this an upgrade would
50
+ * try to stage a new build on top of the running one.
51
+ */
52
+ export declare function stagedName(channel: ReleaseChannel, version: string, revision: string): string;
package/dist/layout.js ADDED
@@ -0,0 +1,96 @@
1
+ import { readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Where an installation lives, and the one small record that describes it.
6
+ *
7
+ * Every version sits under `versions/<version>` and is immutable once
8
+ * verified; `current` is a symbolic link swapped by rename. Nothing the CLI
9
+ * does later edits a versioned directory, so an upgrade that fails can put
10
+ * the link back and be exactly where it started.
11
+ */
12
+ export const UNIT_NAME = 'terminay-server.service';
13
+ export const INSTALL_RECORD = 'install.json';
14
+ export function installLayout(scope, home = homedir()) {
15
+ if (scope === 'system') {
16
+ const prefix = '/opt/terminay';
17
+ return Object.freeze({
18
+ scope,
19
+ prefix,
20
+ versionsDirectory: join(prefix, 'versions'),
21
+ currentLink: join(prefix, 'current'),
22
+ recordPath: join(prefix, INSTALL_RECORD),
23
+ environmentFile: '/etc/terminay/server.env',
24
+ defaultDataRoot: '/var/lib/terminay',
25
+ unitPath: join('/etc/systemd/system', UNIT_NAME),
26
+ });
27
+ }
28
+ const prefix = join(home, '.local/share/terminay');
29
+ return Object.freeze({
30
+ scope,
31
+ prefix,
32
+ versionsDirectory: join(prefix, 'versions'),
33
+ currentLink: join(prefix, 'current'),
34
+ recordPath: join(prefix, INSTALL_RECORD),
35
+ environmentFile: join(home, '.config/terminay/server.env'),
36
+ defaultDataRoot: join(prefix, 'data'),
37
+ unitPath: join(home, '.config/systemd/user', UNIT_NAME),
38
+ });
39
+ }
40
+ export function parseInstallRecord(value) {
41
+ if (typeof value !== 'object' || value === null)
42
+ throw new Error('the install record is not readable');
43
+ const record = value;
44
+ if (record.schemaVersion !== 1)
45
+ throw new Error('the install record has an unsupported schema version');
46
+ for (const field of [
47
+ 'scope',
48
+ 'channel',
49
+ 'version',
50
+ 'revision',
51
+ 'runAs',
52
+ 'dataRoot',
53
+ 'projectRoot',
54
+ 'expose',
55
+ 'hostedDomain',
56
+ ]) {
57
+ if (typeof record[field] !== 'string' ||
58
+ record[field].length === 0) {
59
+ throw new Error(`the install record is missing ${field}`);
60
+ }
61
+ }
62
+ if (!Number.isSafeInteger(record.port) ||
63
+ !Number.isSafeInteger(record.healthPort)) {
64
+ throw new Error('the install record is missing its ports');
65
+ }
66
+ return record;
67
+ }
68
+ export async function readInstallRecord(layout) {
69
+ const raw = await readFile(layout.recordPath, 'utf8').catch(() => undefined);
70
+ if (raw === undefined)
71
+ return undefined;
72
+ return parseInstallRecord(JSON.parse(raw));
73
+ }
74
+ export async function writeInstallRecord(layout, record) {
75
+ // Replaced by rename so a command interrupted mid-write never leaves a
76
+ // half-written record that the next command would refuse to parse.
77
+ const temporary = `${layout.recordPath}.tmp`;
78
+ await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, {
79
+ mode: 0o644,
80
+ });
81
+ await rename(temporary, layout.recordPath);
82
+ }
83
+ export function versionDirectory(layout, version) {
84
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(version))
85
+ throw new Error(`unsafe version directory name: ${version}`);
86
+ return join(layout.versionsDirectory, version);
87
+ }
88
+ /**
89
+ * The rolling channel reuses the version string `main` for every build, so its
90
+ * directories are named for the commit instead. Without this an upgrade would
91
+ * try to stage a new build on top of the running one.
92
+ */
93
+ export function stagedName(channel, version, revision) {
94
+ return channel === 'tag' ? version : `${channel}-${revision.slice(0, 12)}`;
95
+ }
96
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { Architecture, ReleaseChannel } from './resolve.js';
2
+ /**
3
+ * The manifest inside a server archive, and what makes it trustworthy.
4
+ *
5
+ * `scripts/build-standalone-server-artifact.mjs` writes this file, and the
6
+ * signature the CLI already checked covers it, so re-hashing the unpacked tree
7
+ * against it turns "the download was authentic" into "what landed on disk is
8
+ * what was signed". A tar that unpacked partially, or a versioned directory
9
+ * something later edited, is caught here rather than at first start.
10
+ */
11
+ export declare const ARCHIVE_MANIFEST = "artifact-manifest.json";
12
+ export interface ManifestFile {
13
+ readonly path: string;
14
+ readonly mode: string;
15
+ readonly size: number;
16
+ readonly sha256: string;
17
+ }
18
+ export interface ArchiveManifest {
19
+ readonly schemaVersion: 1;
20
+ readonly artifact: 'terminay-server';
21
+ readonly target: string;
22
+ readonly channel: ReleaseChannel;
23
+ readonly revision: string;
24
+ readonly architecture: Architecture;
25
+ readonly version: string;
26
+ readonly entrypoints: Readonly<{
27
+ server: string;
28
+ }>;
29
+ readonly files: readonly ManifestFile[];
30
+ }
31
+ export declare class ManifestError extends Error {
32
+ }
33
+ export declare function parseArchiveManifest(value: unknown): ArchiveManifest;
34
+ export declare function readArchiveManifest(root: string): Promise<ArchiveManifest>;
35
+ export interface ExpectedRelease {
36
+ readonly channel?: ReleaseChannel;
37
+ readonly architecture?: Architecture;
38
+ readonly version?: string;
39
+ readonly revision?: string;
40
+ }
41
+ /**
42
+ * Re-hash the unpacked tree against its manifest. Symbolic links are refused
43
+ * outright: the manifest describes regular files, and a link where a file is
44
+ * expected is how an archive escapes its own directory.
45
+ */
46
+ export declare function validateUnpackedArchive(root: string, expected?: ExpectedRelease): Promise<ArchiveManifest>;
@@ -0,0 +1,115 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { lstat, readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ /**
7
+ * The manifest inside a server archive, and what makes it trustworthy.
8
+ *
9
+ * `scripts/build-standalone-server-artifact.mjs` writes this file, and the
10
+ * signature the CLI already checked covers it, so re-hashing the unpacked tree
11
+ * against it turns "the download was authentic" into "what landed on disk is
12
+ * what was signed". A tar that unpacked partially, or a versioned directory
13
+ * something later edited, is caught here rather than at first start.
14
+ */
15
+ export const ARCHIVE_MANIFEST = 'artifact-manifest.json';
16
+ export class ManifestError extends Error {
17
+ }
18
+ function fail(message) {
19
+ throw new ManifestError(`the installed archive is not valid: ${message}`);
20
+ }
21
+ const SAFE_PATH = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[^\\]+$/u;
22
+ export function parseArchiveManifest(value) {
23
+ if (typeof value !== 'object' || value === null)
24
+ fail('its manifest is not an object');
25
+ const manifest = value;
26
+ if (manifest.schemaVersion !== 1)
27
+ fail('its manifest has an unsupported schema version');
28
+ if (manifest.artifact !== 'terminay-server')
29
+ fail(`its manifest describes ${String(manifest.artifact)}`);
30
+ for (const field of [
31
+ 'target',
32
+ 'revision',
33
+ 'version',
34
+ 'channel',
35
+ 'architecture',
36
+ ]) {
37
+ if (typeof manifest[field] !== 'string' ||
38
+ manifest[field].length === 0)
39
+ fail(`its manifest is missing ${field}`);
40
+ }
41
+ if (!/^[a-f0-9]{40}$/u.test(manifest.revision))
42
+ fail('its manifest revision is not a commit sha');
43
+ const entrypoints = manifest.entrypoints;
44
+ if (typeof entrypoints?.server !== 'string')
45
+ fail('its manifest names no server entrypoint');
46
+ if (!Array.isArray(manifest.files) || manifest.files.length === 0)
47
+ fail('its manifest lists no files');
48
+ for (const file of manifest.files) {
49
+ const entry = file;
50
+ if (typeof entry.path !== 'string' || !SAFE_PATH.test(entry.path))
51
+ fail(`its manifest lists an unsafe path: ${String(entry.path)}`);
52
+ if (!/^[a-f0-9]{64}$/u.test(String(entry.sha256)))
53
+ fail(`its manifest has no digest for ${entry.path}`);
54
+ if (!Number.isSafeInteger(entry.size))
55
+ fail(`its manifest has no size for ${entry.path}`);
56
+ if (!/^[0-7]{3}$/u.test(String(entry.mode)))
57
+ fail(`its manifest has no mode for ${entry.path}`);
58
+ }
59
+ return manifest;
60
+ }
61
+ export async function readArchiveManifest(root) {
62
+ const raw = await readFile(join(root, ARCHIVE_MANIFEST), 'utf8').catch(() => undefined);
63
+ if (raw === undefined)
64
+ fail(`it has no ${ARCHIVE_MANIFEST}`);
65
+ try {
66
+ return parseArchiveManifest(JSON.parse(raw));
67
+ }
68
+ catch (error) {
69
+ if (error instanceof ManifestError)
70
+ throw error;
71
+ fail(`its ${ARCHIVE_MANIFEST} is not valid JSON`);
72
+ }
73
+ }
74
+ async function digest(path) {
75
+ const hash = createHash('sha256');
76
+ await pipeline(createReadStream(path), hash);
77
+ return hash.digest('hex');
78
+ }
79
+ /**
80
+ * Re-hash the unpacked tree against its manifest. Symbolic links are refused
81
+ * outright: the manifest describes regular files, and a link where a file is
82
+ * expected is how an archive escapes its own directory.
83
+ */
84
+ export async function validateUnpackedArchive(root, expected = {}) {
85
+ const manifest = await readArchiveManifest(root);
86
+ for (const [field, wanted] of Object.entries(expected)) {
87
+ if (wanted === undefined)
88
+ continue;
89
+ const actual = manifest[field];
90
+ if (actual !== wanted)
91
+ fail(`its manifest records ${field} ${String(actual)}, but ${String(wanted)} was expected`);
92
+ }
93
+ for (const file of manifest.files) {
94
+ const absolute = join(root, file.path);
95
+ const info = await lstat(absolute).catch(() => undefined);
96
+ if (info === undefined)
97
+ fail(`${file.path} is missing`);
98
+ if (info.isSymbolicLink())
99
+ fail(`${file.path} is a symbolic link`);
100
+ if (!info.isFile())
101
+ fail(`${file.path} is not a regular file`);
102
+ if (info.size !== file.size)
103
+ fail(`${file.path} is ${info.size} bytes, but its manifest records ${file.size}`);
104
+ if ((await digest(absolute)) !== file.sha256)
105
+ fail(`${file.path} does not match the digest its manifest records`);
106
+ }
107
+ const entrypoint = join(root, manifest.entrypoints.server);
108
+ const launcher = await lstat(entrypoint).catch(() => undefined);
109
+ if (launcher === undefined || !launcher.isFile())
110
+ fail(`its server entrypoint ${manifest.entrypoints.server} is missing`);
111
+ if ((launcher.mode & 0o111) === 0)
112
+ fail(`its server entrypoint ${manifest.entrypoints.server} is not executable`);
113
+ return manifest;
114
+ }
115
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The `daemon` commands drive systemd and install a Linux server archive, so
3
+ * they refuse anywhere that combination cannot exist. The refusal names the
4
+ * requirement rather than the failure, because an operator on macOS needs to
5
+ * know the CLI is for the box they are deploying to, not the one they typed on.
6
+ */
7
+ export declare const SYSTEMD_MARKER = "/run/systemd/system";
8
+ export interface HostFacts {
9
+ readonly platform: string;
10
+ readonly arch: string;
11
+ readonly hasSystemd: boolean;
12
+ }
13
+ export declare function readHostFacts(): HostFacts;
14
+ export declare class UnsupportedHostError extends Error {
15
+ }
16
+ export declare function assertSupportedHost(facts?: HostFacts): void;
17
+ export declare function hostArchitecture(facts?: HostFacts): 'x64' | 'arm64';
@@ -0,0 +1,34 @@
1
+ import { existsSync } from 'node:fs';
2
+ /**
3
+ * The `daemon` commands drive systemd and install a Linux server archive, so
4
+ * they refuse anywhere that combination cannot exist. The refusal names the
5
+ * requirement rather than the failure, because an operator on macOS needs to
6
+ * know the CLI is for the box they are deploying to, not the one they typed on.
7
+ */
8
+ export const SYSTEMD_MARKER = '/run/systemd/system';
9
+ const SUPPORTED_ARCHITECTURES = ['x64', 'arm64'];
10
+ export function readHostFacts() {
11
+ return Object.freeze({
12
+ platform: process.platform,
13
+ arch: process.arch,
14
+ hasSystemd: existsSync(SYSTEMD_MARKER),
15
+ });
16
+ }
17
+ export class UnsupportedHostError extends Error {
18
+ }
19
+ export function assertSupportedHost(facts = readHostFacts()) {
20
+ if (facts.platform !== 'linux') {
21
+ throw new UnsupportedHostError(`terminay daemon runs on 64-bit Linux with systemd; this host is ${facts.platform}. Run it on the server you are deploying to.`);
22
+ }
23
+ if (!SUPPORTED_ARCHITECTURES.includes(facts.arch)) {
24
+ throw new UnsupportedHostError(`terminay daemon supports the x64 and arm64 architectures; this host is ${facts.arch}.`);
25
+ }
26
+ if (!facts.hasSystemd) {
27
+ throw new UnsupportedHostError(`terminay daemon requires systemd as the service manager; ${SYSTEMD_MARKER} is absent on this host.`);
28
+ }
29
+ }
30
+ export function hostArchitecture(facts = readHostFacts()) {
31
+ assertSupportedHost(facts);
32
+ return facts.arch;
33
+ }
34
+ //# sourceMappingURL=platform.js.map