@nosana/ssh 2.13.1

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 ADDED
@@ -0,0 +1,51 @@
1
+ # @nosana/ssh
2
+
3
+ The local side of SSH access to Nosana jobs, with no network code:
4
+
5
+ - generate OpenSSH Ed25519 key pairs in any runtime with Web Crypto;
6
+ - validate and parse `authorized_keys`-style public keys with the node's limits;
7
+ - read and update the SSH keys of a job definition;
8
+ - describe the `ssh` command that reaches a job through its node's proxy.
9
+
10
+ Talking to a node, which is where keys are granted and web terminals open,
11
+ lives in `@nosana/api` under `api.jobs(job)`. The kit exposes it as
12
+ `client.api.jobs(job)` and re-exports this package's tools.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @nosana/ssh
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import {
24
+ createSshCommand,
25
+ generateSshKeyPair,
26
+ parseSshPublicKeys,
27
+ withSshPublicKeys,
28
+ } from '@nosana/ssh';
29
+
30
+ const pair = await generateSshKeyPair({ comment: 'laptop' });
31
+
32
+ const { keys, errors } = parseSshPublicKeys(authorizedKeysText);
33
+ const definition = withSshPublicKeys(jobDefinition, keys);
34
+
35
+ const command = createSshCommand(
36
+ { job: jobAddress, node: nodeAddress, nodeDomain: 'node.k8s.prd.nos.ci' },
37
+ { identityFile: '~/.ssh/id_ed25519' }, // the private half of the key the node was given
38
+ );
39
+ console.log(command.formattedCommand);
40
+ // ssh ... nosana@<jobAddress>-0-ssh.node.k8s.prd.nos.ci
41
+ ```
42
+
43
+ Every operation of a job has its own SSH sidecar; `opIndex` (the operation's
44
+ position in the job definition's `ops`, default 0) picks which one the command
45
+ reaches. The user is always `nosana`. Only interactive shells are served: no
46
+ remote commands, `sftp`, `scp` or forwarding.
47
+
48
+ A key line is at most one line; a job holds at most 10 keys and 64 KiB of key
49
+ data. `parseSshPublicKeys` reports every problem rather than throwing, and
50
+ `requireSshPublicKey` / `requireSshPublicKeySet` throw for callers that want
51
+ validation before sending.
@@ -0,0 +1,10 @@
1
+ import type { SshCommandOptions, SshConnectionDescriptor, SshJobTarget } from './types.js';
2
+ /** The only user name a job's SSH sidecar accepts; the hostname names the job. */
3
+ export declare const SSH_USERNAME = "nosana";
4
+ /** The hostname of one operation's SSH sidecar, as published through the node's proxy. */
5
+ export declare function sshHostname(target: SshJobTarget, opIndex?: number): string;
6
+ /** Describes the `ssh` invocation that reaches a job through its node's proxy. */
7
+ export declare function createSshCommand(target: SshJobTarget, options?: SshCommandOptions): SshConnectionDescriptor;
8
+ /** Joins an executable and its arguments into a POSIX shell command that can be pasted as is. */
9
+ export declare function formatSshCommand(executable: string, args: string[]): string;
10
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI3F,kFAAkF;AAClF,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC,0FAA0F;AAC1F,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,SAAI,GAAG,MAAM,CAKrE;AAED,kFAAkF;AAClF,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,iBAAsB,GAC9B,uBAAuB,CAqBzB;AAWD,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAE3E"}
@@ -0,0 +1,45 @@
1
+ const DEFAULT_PROXY_PORT = 5002;
2
+ /** The only user name a job's SSH sidecar accepts; the hostname names the job. */
3
+ export const SSH_USERNAME = 'nosana';
4
+ /** The hostname of one operation's SSH sidecar, as published through the node's proxy. */
5
+ export function sshHostname(target, opIndex = 0) {
6
+ if (!Number.isInteger(opIndex) || opIndex < 0) {
7
+ throw new Error('SSH operation index must be a non-negative integer.');
8
+ }
9
+ return `${target.job}-${opIndex}-ssh.${target.nodeDomain}`;
10
+ }
11
+ /** Describes the `ssh` invocation that reaches a job through its node's proxy. */
12
+ export function createSshCommand(target, options = {}) {
13
+ const username = SSH_USERNAME;
14
+ const hostname = sshHostname(target, options.opIndex);
15
+ const proxyCommand = proxyCommandFor(target.nodeDomain, options);
16
+ const args = [
17
+ '-o',
18
+ 'StrictHostKeyChecking=accept-new',
19
+ '-o',
20
+ `ProxyCommand=${proxyCommand}`,
21
+ ...(options.identityFile ? ['-i', options.identityFile] : []),
22
+ `${username}@${hostname}`,
23
+ ];
24
+ return {
25
+ executable: 'ssh',
26
+ username,
27
+ hostname,
28
+ proxyCommand,
29
+ args,
30
+ formattedCommand: formatSshCommand('ssh', args),
31
+ };
32
+ }
33
+ function proxyCommandFor(nodeDomain, { proxyMode = 'socat', proxyPort = DEFAULT_PROXY_PORT }) {
34
+ return proxyMode === 'nc'
35
+ ? `nc -X connect -x ${nodeDomain}:${proxyPort} %h %p`
36
+ : `socat - PROXY:${nodeDomain}:%h:%p,proxyport=${proxyPort}`;
37
+ }
38
+ /** Joins an executable and its arguments into a POSIX shell command that can be pasted as is. */
39
+ export function formatSshCommand(executable, args) {
40
+ return [executable, ...args].map(shellQuote).join(' ');
41
+ }
42
+ function shellQuote(value) {
43
+ return /^[A-Za-z0-9_/:=.,%+@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
44
+ }
45
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAEA,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,kFAAkF;AAClF,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC;AAErC,0FAA0F;AAC1F,MAAM,UAAU,WAAW,CAAC,MAAoB,EAAE,OAAO,GAAG,CAAC;IAC3D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,OAAO,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC;AAC7D,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,gBAAgB,CAC9B,MAAoB,EACpB,UAA6B,EAAE;IAE/B,MAAM,QAAQ,GAAG,YAAY,CAAC;IAC9B,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,YAAY,GAAG,eAAe,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG;QACX,IAAI;QACJ,kCAAkC;QAClC,IAAI;QACJ,gBAAgB,YAAY,EAAE;QAC9B,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,GAAG,QAAQ,IAAI,QAAQ,EAAE;KAC1B,CAAC;IAEF,OAAO;QACL,UAAU,EAAE,KAAK;QACjB,QAAQ;QACR,QAAQ;QACR,YAAY;QACZ,IAAI;QACJ,gBAAgB,EAAE,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC;KAChD,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CACtB,UAAkB,EAClB,EAAE,SAAS,GAAG,OAAO,EAAE,SAAS,GAAG,kBAAkB,EAAqB;IAE1E,OAAO,SAAS,KAAK,IAAI;QACvB,CAAC,CAAC,oBAAoB,UAAU,IAAI,SAAS,QAAQ;QACrD,CAAC,CAAC,iBAAiB,UAAU,oBAAoB,SAAS,EAAE,CAAC;AACjE,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,IAAc;IACjE,OAAO,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,0BAA0B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAC9F,CAAC"}
@@ -0,0 +1,10 @@
1
+ /** Byte helpers for the OpenSSH wire format and strict base64 text. */
2
+ export declare function concatBytes(...chunks: Uint8Array[]): Uint8Array;
3
+ export declare function encodeUint32(value: number): Uint8Array;
4
+ export declare function readUint32(bytes: Uint8Array, offset: number): number | undefined;
5
+ /** An SSH `string`: a big-endian uint32 length followed by the bytes. */
6
+ export declare function encodeSshString(value: Uint8Array): Uint8Array;
7
+ export declare function bytesToBase64(bytes: Uint8Array): string;
8
+ /** Decodes canonical base64 only: the bits an encoder leaves unused before the padding must be zero. */
9
+ export declare function base64ToBytes(value: string): Uint8Array | undefined;
10
+ //# sourceMappingURL=encoding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,wBAAgB,WAAW,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAQ/D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAItD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGhF;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAE7D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAIvD;AAKD,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAGnE"}
@@ -0,0 +1,46 @@
1
+ /** Byte helpers for the OpenSSH wire format and strict base64 text. */
2
+ export function concatBytes(...chunks) {
3
+ const result = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
4
+ let offset = 0;
5
+ for (const chunk of chunks) {
6
+ result.set(chunk, offset);
7
+ offset += chunk.length;
8
+ }
9
+ return result;
10
+ }
11
+ export function encodeUint32(value) {
12
+ const bytes = new Uint8Array(4);
13
+ new DataView(bytes.buffer).setUint32(0, value);
14
+ return bytes;
15
+ }
16
+ export function readUint32(bytes, offset) {
17
+ if (offset < 0 || offset + 4 > bytes.length)
18
+ return undefined;
19
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset);
20
+ }
21
+ /** An SSH `string`: a big-endian uint32 length followed by the bytes. */
22
+ export function encodeSshString(value) {
23
+ return concatBytes(encodeUint32(value.length), value);
24
+ }
25
+ export function bytesToBase64(bytes) {
26
+ let binary = '';
27
+ for (const byte of bytes)
28
+ binary += String.fromCharCode(byte);
29
+ return btoa(binary);
30
+ }
31
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
32
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
33
+ /** Decodes canonical base64 only: the bits an encoder leaves unused before the padding must be zero. */
34
+ export function base64ToBytes(value) {
35
+ if (value.length === 0 || !BASE64.test(value) || !hasCleanTail(value))
36
+ return undefined;
37
+ return Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
38
+ }
39
+ function hasCleanTail(value) {
40
+ const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
41
+ if (padding === 0)
42
+ return true;
43
+ const last = ALPHABET.indexOf(value[value.length - padding - 1]);
44
+ return (last & (padding === 2 ? 0b1111 : 0b11)) === 0;
45
+ }
46
+ //# sourceMappingURL=encoding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoding.js","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,MAAM,UAAU,WAAW,CAAC,GAAG,MAAoB;IACjD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACxF,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;IACzB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAChC,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAiB,EAAE,MAAc;IAC1D,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9D,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAC1F,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,KAAiB;IAC/C,OAAO,WAAW,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,MAAM,GAAG,kEAAkE,CAAC;AAClF,MAAM,QAAQ,GAAG,kEAAkE,CAAC;AAEpF,wGAAwG;AACxG,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACxF,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,IAAI,OAAO,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;IACjE,OAAO,CAAC,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Validates one SSH grant expiry the way the node's sidecar contract needs it:
3
+ * a strict ISO 8601 UTC timestamp, in the future, that JavaScript does not
4
+ * silently normalize (so `2025-02-30` is rejected rather than rolled forward).
5
+ * Returns the canonical ISO string, or throws.
6
+ */
7
+ export declare function requireFutureSshExpiry(value: unknown, now?: number): string;
8
+ //# sourceMappingURL=expiry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expiry.d.ts","sourceRoot":"","sources":["../src/expiry.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAavF"}
package/dist/expiry.js ADDED
@@ -0,0 +1,23 @@
1
+ /** ISO 8601 UTC, seconds precision with optional milliseconds: `2025-01-02T03:04:05Z`. */
2
+ const ISO_8601_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
3
+ /**
4
+ * Validates one SSH grant expiry the way the node's sidecar contract needs it:
5
+ * a strict ISO 8601 UTC timestamp, in the future, that JavaScript does not
6
+ * silently normalize (so `2025-02-30` is rejected rather than rolled forward).
7
+ * Returns the canonical ISO string, or throws.
8
+ */
9
+ export function requireFutureSshExpiry(value, now = Date.now()) {
10
+ if (typeof value !== 'string' || !ISO_8601_UTC.test(value)) {
11
+ throw new Error('SSH expiry must be an ISO 8601 UTC timestamp.');
12
+ }
13
+ const date = new Date(value);
14
+ if (!Number.isFinite(date.getTime()) || date.getTime() <= now) {
15
+ throw new Error('SSH expiry must be in the future.');
16
+ }
17
+ // A real date round-trips; a normalized one (for example February 30) does not.
18
+ if (date.toISOString().slice(0, 19) !== value.slice(0, 19)) {
19
+ throw new Error('SSH expiry is not a valid date.');
20
+ }
21
+ return date.toISOString();
22
+ }
23
+ //# sourceMappingURL=expiry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expiry.js","sourceRoot":"","sources":["../src/expiry.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,MAAM,YAAY,GAAG,sDAAsD,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAc,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC7E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IACD,gFAAgF;IAChF,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,10 @@
1
+ export { createSshCommand, formatSshCommand, sshHostname, SSH_USERNAME } from './connection.js';
2
+ export { bytesToBase64 } from './encoding.js';
3
+ export { requireFutureSshExpiry } from './expiry.js';
4
+ export { getSshPublicKeys, withSshPublicKeys } from './jobDefinition.js';
5
+ export { generateSshKeyPair, SSH_ED25519 } from './keyPair.js';
6
+ export { getSshKeyIdentity, isValidSshPublicKey, parseSshPublicKeys, requireSshPublicKey, requireSshPublicKeySet, MAX_SSH_PUBLIC_KEYS, MAX_SSH_PUBLIC_KEY_LINE_LENGTH, MAX_SSH_PUBLIC_KEYS_BYTES, SSH_PUBLIC_KEY_ALGORITHMS, } from './publicKeys.js';
7
+ export { buildTerminalAuthorizationMessage, isSafeTerminalOperation, parseTerminalAuthorizationMessage, TERMINAL_AUTHORIZATION_AUDIENCE, TERMINAL_AUTHORIZATION_MAX_TTL_MS, TERMINAL_AUTHORIZATION_MESSAGE_HEADER, } from './terminalAuthorization.js';
8
+ export type { ParsedTerminalAuthorizationMessage, TerminalAuthorizationMessageOptions, } from './terminalAuthorization.js';
9
+ export type * from './types.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,8BAA8B,EAC9B,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iCAAiC,EACjC,uBAAuB,EACvB,iCAAiC,EACjC,+BAA+B,EAC/B,iCAAiC,EACjC,qCAAqC,GACtC,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,kCAAkC,EAClC,mCAAmC,GACpC,MAAM,4BAA4B,CAAC;AACpC,mBAAmB,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createSshCommand, formatSshCommand, sshHostname, SSH_USERNAME } from './connection.js';
2
+ export { bytesToBase64 } from './encoding.js';
3
+ export { requireFutureSshExpiry } from './expiry.js';
4
+ export { getSshPublicKeys, withSshPublicKeys } from './jobDefinition.js';
5
+ export { generateSshKeyPair, SSH_ED25519 } from './keyPair.js';
6
+ export { getSshKeyIdentity, isValidSshPublicKey, parseSshPublicKeys, requireSshPublicKey, requireSshPublicKeySet, MAX_SSH_PUBLIC_KEYS, MAX_SSH_PUBLIC_KEY_LINE_LENGTH, MAX_SSH_PUBLIC_KEYS_BYTES, SSH_PUBLIC_KEY_ALGORITHMS, } from './publicKeys.js';
7
+ export { buildTerminalAuthorizationMessage, isSafeTerminalOperation, parseTerminalAuthorizationMessage, TERMINAL_AUTHORIZATION_AUDIENCE, TERMINAL_AUTHORIZATION_MAX_TTL_MS, TERMINAL_AUTHORIZATION_MESSAGE_HEADER, } from './terminalAuthorization.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,8BAA8B,EAC9B,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iCAAiC,EACjC,uBAAuB,EACvB,iCAAiC,EACjC,+BAA+B,EAC/B,iCAAiC,EACjC,qCAAqC,GACtC,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { JobDefinition } from '@nosana/types';
2
+ /** The SSH public keys a job definition grants access to, ignoring blank entries. */
3
+ export declare function getSshPublicKeys(definition: JobDefinition): string[];
4
+ /** A copy of `definition` granting exactly `publicKeys`; an empty list drops the `ssh` block. */
5
+ export declare function withSshPublicKeys(definition: JobDefinition, publicKeys: string[]): JobDefinition;
6
+ //# sourceMappingURL=jobDefinition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobDefinition.d.ts","sourceRoot":"","sources":["../src/jobDefinition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,qFAAqF;AACrF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,aAAa,GAAG,MAAM,EAAE,CAIpE;AAED,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,aAAa,CAIhG"}
@@ -0,0 +1,15 @@
1
+ /** The SSH public keys a job definition grants access to, ignoring blank entries. */
2
+ export function getSshPublicKeys(definition) {
3
+ const keys = definition.ssh?.public_keys;
4
+ if (!Array.isArray(keys))
5
+ return [];
6
+ return keys.filter((key) => typeof key === 'string' && key.trim().length > 0);
7
+ }
8
+ /** A copy of `definition` granting exactly `publicKeys`; an empty list drops the `ssh` block. */
9
+ export function withSshPublicKeys(definition, publicKeys) {
10
+ const { ssh, ...rest } = definition;
11
+ if (publicKeys.length === 0)
12
+ return rest;
13
+ return { ...rest, ssh: { ...ssh, public_keys: [...publicKeys] } };
14
+ }
15
+ //# sourceMappingURL=jobDefinition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobDefinition.js","sourceRoot":"","sources":["../src/jobDefinition.ts"],"names":[],"mappings":"AAEA,qFAAqF;AACrF,MAAM,UAAU,gBAAgB,CAAC,UAAyB;IACxD,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,EAAE,WAAW,CAAC;IACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,iBAAiB,CAAC,UAAyB,EAAE,UAAoB;IAC/E,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,UAAU,CAAC;IACpC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,GAAG,GAAG,EAAE,WAAW,EAAE,CAAC,GAAG,UAAU,CAAC,EAAE,EAAE,CAAC;AACpE,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { GenerateSshKeyPairOptions, SshKeyPair } from './types.js';
2
+ export declare const SSH_ED25519 = "ssh-ed25519";
3
+ /** Generates an unencrypted, OpenSSH-formatted Ed25519 key pair. */
4
+ export declare function generateSshKeyPair(options?: GenerateSshKeyPairOptions): Promise<SshKeyPair>;
5
+ //# sourceMappingURL=keyPair.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyPair.d.ts","sourceRoot":"","sources":["../src/keyPair.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,yBAAyB,EAAqB,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3F,eAAO,MAAM,WAAW,gBAAgB,CAAC;AASzC,oEAAoE;AACpE,wBAAsB,kBAAkB,CACtC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,UAAU,CAAC,CAkBrB"}
@@ -0,0 +1,47 @@
1
+ import nacl from 'tweetnacl';
2
+ import { bytesToBase64, concatBytes, encodeSshString, encodeUint32 } from './encoding.js';
3
+ export const SSH_ED25519 = 'ssh-ed25519';
4
+ const OPENSSH_KEY_MAGIC = 'openssh-key-v1\0';
5
+ const OPENSSH_KEY_BLOCK_SIZE = 8;
6
+ const DEFAULT_COMMENT = 'nosana';
7
+ const PEM_LINE_LENGTH = 64;
8
+ const encoder = new TextEncoder();
9
+ /** Generates an unencrypted, OpenSSH-formatted Ed25519 key pair. */
10
+ export async function generateSshKeyPair(options = {}) {
11
+ const random = randomSource(options.cryptoProvider ?? globalThis.crypto);
12
+ const seed = random(32);
13
+ const { publicKey } = nacl.sign.keyPair.fromSeed(seed);
14
+ const comment = normalizeComment(options.comment);
15
+ const publicKeyBlob = concatBytes(encodeSshString(encoder.encode(SSH_ED25519)), encodeSshString(publicKey));
16
+ return {
17
+ algorithm: SSH_ED25519,
18
+ publicKey: [SSH_ED25519, bytesToBase64(publicKeyBlob), comment].filter(Boolean).join(' '),
19
+ privateKey: toPem('OPENSSH PRIVATE KEY', encodePrivateKey({ seed, publicKey, publicKeyBlob, comment, checkInteger: random(4) })),
20
+ };
21
+ }
22
+ function randomSource(provider) {
23
+ if (typeof provider?.getRandomValues !== 'function') {
24
+ throw new Error('Secure SSH key generation is not supported in this runtime.');
25
+ }
26
+ return (length) => provider.getRandomValues(new Uint8Array(length));
27
+ }
28
+ /** OpenSSH comments are one word; whitespace would split the key line. */
29
+ function normalizeComment(comment) {
30
+ return (comment ?? DEFAULT_COMMENT).trim().replace(/\s+/g, '-');
31
+ }
32
+ /** The `openssh-key-v1` container holding one unencrypted key. */
33
+ function encodePrivateKey(parts) {
34
+ const privateSection = concatBytes(parts.checkInteger, parts.checkInteger, encodeSshString(encoder.encode(SSH_ED25519)), encodeSshString(parts.publicKey), encodeSshString(concatBytes(parts.seed, parts.publicKey)), encodeSshString(encoder.encode(parts.comment)));
35
+ const paddingLength = OPENSSH_KEY_BLOCK_SIZE - (privateSection.length % OPENSSH_KEY_BLOCK_SIZE);
36
+ const padding = Uint8Array.from({ length: paddingLength }, (_, index) => index + 1);
37
+ return concatBytes(encoder.encode(OPENSSH_KEY_MAGIC), encodeSshString(encoder.encode('none')), // cipher
38
+ encodeSshString(encoder.encode('none')), // key derivation
39
+ encodeSshString(new Uint8Array()), // key derivation options
40
+ encodeUint32(1), // number of keys
41
+ encodeSshString(parts.publicKeyBlob), encodeSshString(concatBytes(privateSection, padding)));
42
+ }
43
+ function toPem(label, bytes) {
44
+ const lines = bytesToBase64(bytes).match(new RegExp(`.{1,${PEM_LINE_LENGTH}}`, 'g')) ?? [];
45
+ return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`;
46
+ }
47
+ //# sourceMappingURL=keyPair.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyPair.js","sourceRoot":"","sources":["../src/keyPair.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { SshPublicKeyParseResult } from './types.js';
2
+ export declare const MAX_SSH_PUBLIC_KEYS = 10;
3
+ export declare const MAX_SSH_PUBLIC_KEY_LINE_LENGTH = 8192;
4
+ export declare const MAX_SSH_PUBLIC_KEYS_BYTES: number;
5
+ export declare const SSH_PUBLIC_KEY_ALGORITHMS: ReadonlySet<string>;
6
+ /**
7
+ * Whether `value` is one OpenSSH public-key line: a supported algorithm, a
8
+ * base64 blob that names the same algorithm, and at most one comment word.
9
+ */
10
+ export declare function isValidSshPublicKey(value: string): boolean;
11
+ /**
12
+ * A key's identity for deduplication and revocation: its algorithm and blob,
13
+ * with the comment dropped. Two lines that differ only by comment name the
14
+ * same key, so both the node and the client collapse them to one.
15
+ */
16
+ export declare function getSshKeyIdentity(key: string): string;
17
+ /** Returns the trimmed key, or throws when `value` is not exactly one valid OpenSSH public key. */
18
+ export declare function requireSshPublicKey(value: unknown): string;
19
+ /**
20
+ * Parses one key per line. Blank lines are skipped and duplicates collapsed;
21
+ * every problem is reported rather than thrown so callers can show them all.
22
+ */
23
+ export declare function parseSshPublicKeys(input: string): SshPublicKeyParseResult;
24
+ /** Validates a key set the way the node does, throwing with every problem found. */
25
+ export declare function requireSshPublicKeySet(values: unknown): string[];
26
+ //# sourceMappingURL=publicKeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicKeys.d.ts","sourceRoot":"","sources":["../src/publicKeys.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA0B,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAElF,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,8BAA8B,OAAO,CAAC;AACnD,eAAO,MAAM,yBAAyB,QAAY,CAAC;AAEnD,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAQxD,CAAC;AAKH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAS1D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAWD,mGAAmG;AACnG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAK1D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,uBAAuB,CAmBzE;AAED,oFAAoF;AACpF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAKhE"}
@@ -0,0 +1,114 @@
1
+ import { base64ToBytes, readUint32 } from './encoding.js';
2
+ export const MAX_SSH_PUBLIC_KEYS = 10;
3
+ export const MAX_SSH_PUBLIC_KEY_LINE_LENGTH = 8192;
4
+ export const MAX_SSH_PUBLIC_KEYS_BYTES = 64 * 1024;
5
+ export const SSH_PUBLIC_KEY_ALGORITHMS = new Set([
6
+ 'ssh-ed25519',
7
+ 'ssh-rsa',
8
+ 'ecdsa-sha2-nistp256',
9
+ 'ecdsa-sha2-nistp384',
10
+ 'ecdsa-sha2-nistp521',
11
+ 'sk-ssh-ed25519@openssh.com',
12
+ 'sk-ecdsa-sha2-nistp256@openssh.com',
13
+ ]);
14
+ const encoder = new TextEncoder();
15
+ const decoder = new TextDecoder();
16
+ /**
17
+ * Whether `value` is one OpenSSH public-key line: a supported algorithm, a
18
+ * base64 blob that names the same algorithm, and at most one comment word.
19
+ */
20
+ export function isValidSshPublicKey(value) {
21
+ const key = value.trim();
22
+ if (!key || key.length > MAX_SSH_PUBLIC_KEY_LINE_LENGTH)
23
+ return false;
24
+ // One line only: an embedded newline could smuggle a second authorized_keys entry.
25
+ if (/[\r\n]/.test(key))
26
+ return false;
27
+ const [algorithm, blob, , ...rest] = key.split(/\s+/);
28
+ if (rest.length > 0 || !blob || !SSH_PUBLIC_KEY_ALGORITHMS.has(algorithm))
29
+ return false;
30
+ return blobAlgorithm(blob) === algorithm;
31
+ }
32
+ /**
33
+ * A key's identity for deduplication and revocation: its algorithm and blob,
34
+ * with the comment dropped. Two lines that differ only by comment name the
35
+ * same key, so both the node and the client collapse them to one.
36
+ */
37
+ export function getSshKeyIdentity(key) {
38
+ return key.trim().split(/\s+/).slice(0, 2).join(' ');
39
+ }
40
+ /** The algorithm name an OpenSSH public-key blob declares in its first field. */
41
+ function blobAlgorithm(blob) {
42
+ const bytes = base64ToBytes(blob);
43
+ if (!bytes)
44
+ return undefined;
45
+ const length = readUint32(bytes, 0);
46
+ if (!length || 4 + length >= bytes.length)
47
+ return undefined;
48
+ return decoder.decode(bytes.subarray(4, 4 + length));
49
+ }
50
+ /** Returns the trimmed key, or throws when `value` is not exactly one valid OpenSSH public key. */
51
+ export function requireSshPublicKey(value) {
52
+ if (typeof value !== 'string' || !isValidSshPublicKey(value)) {
53
+ throw new Error('A valid OpenSSH public key is required.');
54
+ }
55
+ return value.trim();
56
+ }
57
+ /**
58
+ * Parses one key per line. Blank lines are skipped and duplicates collapsed;
59
+ * every problem is reported rather than thrown so callers can show them all.
60
+ */
61
+ export function parseSshPublicKeys(input) {
62
+ const keys = [];
63
+ const errors = [];
64
+ input.split(/\r?\n/).forEach((line, index) => {
65
+ const key = line.trim();
66
+ if (!key)
67
+ return;
68
+ if (isValidSshPublicKey(key)) {
69
+ keys.push(key);
70
+ }
71
+ else {
72
+ errors.push({
73
+ code: 'INVALID_KEY',
74
+ line: index + 1,
75
+ message: `Line ${index + 1} is not a valid OpenSSH public key.`,
76
+ });
77
+ }
78
+ });
79
+ return collectSshPublicKeys(keys, errors);
80
+ }
81
+ /** Validates a key set the way the node does, throwing with every problem found. */
82
+ export function requireSshPublicKeySet(values) {
83
+ if (!Array.isArray(values))
84
+ throw new Error('Expected an array of SSH public keys.');
85
+ const { keys, errors } = collectSshPublicKeys(values.map(requireSshPublicKey));
86
+ if (errors.length > 0)
87
+ throw new Error(errors.map((error) => error.message).join(' '));
88
+ return keys;
89
+ }
90
+ /** Deduplicates keys that are already valid and reports the set limits the node enforces. */
91
+ function collectSshPublicKeys(keys, errors = []) {
92
+ const byIdentity = new Map();
93
+ for (const key of keys) {
94
+ const identity = getSshKeyIdentity(key);
95
+ if (!byIdentity.has(identity))
96
+ byIdentity.set(identity, key);
97
+ }
98
+ const unique = [...byIdentity.values()];
99
+ if (unique.length > MAX_SSH_PUBLIC_KEYS) {
100
+ errors.push({
101
+ code: 'TOO_MANY_KEYS',
102
+ message: `A job can contain at most ${MAX_SSH_PUBLIC_KEYS} SSH public keys.`,
103
+ });
104
+ }
105
+ const totalBytes = unique.reduce((total, key) => total + encoder.encode(key).length, 0);
106
+ if (totalBytes > MAX_SSH_PUBLIC_KEYS_BYTES) {
107
+ errors.push({
108
+ code: 'KEY_SET_TOO_LARGE',
109
+ message: `SSH public keys can use at most ${MAX_SSH_PUBLIC_KEYS_BYTES} bytes in total.`,
110
+ });
111
+ }
112
+ return { keys: unique, errors };
113
+ }
114
+ //# sourceMappingURL=publicKeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicKeys.js","sourceRoot":"","sources":["../src/publicKeys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The wire contract for a signed, short-lived web-terminal authorization: the
3
+ * exact message the client signs and the node verifies. Both sides import this
4
+ * module so the format, field order and limits cannot drift between them.
5
+ */
6
+ export declare const TERMINAL_AUTHORIZATION_MESSAGE_HEADER = "Nosana Terminal Authorization v1";
7
+ export declare const TERMINAL_AUTHORIZATION_AUDIENCE = "nosana-web-terminal";
8
+ export declare const TERMINAL_AUTHORIZATION_MAX_TTL_MS: number;
9
+ export interface TerminalAuthorizationMessageOptions {
10
+ job: string;
11
+ node: string;
12
+ /** ISO 8601 UTC timestamp. */
13
+ expiresAt: string;
14
+ op?: string;
15
+ network: string;
16
+ }
17
+ export interface ParsedTerminalAuthorizationMessage {
18
+ job: string;
19
+ node: string;
20
+ expiresAt: string;
21
+ op?: string;
22
+ network?: string;
23
+ audience?: string;
24
+ }
25
+ /** An operation selector is interpolated into the signed message, so it must stay one plain token. */
26
+ export declare function isSafeTerminalOperation(op: string): boolean;
27
+ /** The exact text the node verifies; line order is part of the contract. */
28
+ export declare function buildTerminalAuthorizationMessage(options: TerminalAuthorizationMessageOptions): string;
29
+ /**
30
+ * Parses a terminal authorization message back into its fields. Enforces the
31
+ * header and that `job`, `node` and `expiresAt` are present; the caller checks
32
+ * the values and the signature. Duplicate or malformed lines are rejected.
33
+ */
34
+ export declare function parseTerminalAuthorizationMessage(message: string): ParsedTerminalAuthorizationMessage;
35
+ //# sourceMappingURL=terminalAuthorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminalAuthorization.d.ts","sourceRoot":"","sources":["../src/terminalAuthorization.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,qCAAqC,qCAAqC,CAAC;AACxF,eAAO,MAAM,+BAA+B,wBAAwB,CAAC;AACrE,eAAO,MAAM,iCAAiC,QAAgB,CAAC;AAE/D,MAAM,WAAW,mCAAmC;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kCAAkC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,sGAAsG;AACtG,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,4EAA4E;AAC5E,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,mCAAmC,GAC3C,MAAM,CAWR;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,MAAM,GACd,kCAAkC,CA8BpC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The wire contract for a signed, short-lived web-terminal authorization: the
3
+ * exact message the client signs and the node verifies. Both sides import this
4
+ * module so the format, field order and limits cannot drift between them.
5
+ */
6
+ export const TERMINAL_AUTHORIZATION_MESSAGE_HEADER = 'Nosana Terminal Authorization v1';
7
+ export const TERMINAL_AUTHORIZATION_AUDIENCE = 'nosana-web-terminal';
8
+ export const TERMINAL_AUTHORIZATION_MAX_TTL_MS = 5 * 60 * 1000;
9
+ /** An operation selector is interpolated into the signed message, so it must stay one plain token. */
10
+ export function isSafeTerminalOperation(op) {
11
+ return /^[A-Za-z0-9._:-]+$/.test(op);
12
+ }
13
+ /** The exact text the node verifies; line order is part of the contract. */
14
+ export function buildTerminalAuthorizationMessage(options) {
15
+ return [
16
+ TERMINAL_AUTHORIZATION_MESSAGE_HEADER,
17
+ '',
18
+ `job: ${options.job}`,
19
+ `node: ${options.node}`,
20
+ `expiresAt: ${options.expiresAt}`,
21
+ ...(options.op ? [`op: ${options.op}`] : []),
22
+ `network: ${options.network}`,
23
+ `audience: ${TERMINAL_AUTHORIZATION_AUDIENCE}`,
24
+ ].join('\n');
25
+ }
26
+ /**
27
+ * Parses a terminal authorization message back into its fields. Enforces the
28
+ * header and that `job`, `node` and `expiresAt` are present; the caller checks
29
+ * the values and the signature. Duplicate or malformed lines are rejected.
30
+ */
31
+ export function parseTerminalAuthorizationMessage(message) {
32
+ const lines = message.split(/\r?\n/);
33
+ if (lines[0] !== TERMINAL_AUTHORIZATION_MESSAGE_HEADER) {
34
+ throw new Error('Invalid terminal authorization message header');
35
+ }
36
+ const fields = {};
37
+ for (const line of lines.slice(1)) {
38
+ if (!line.trim())
39
+ continue;
40
+ const separator = line.indexOf(':');
41
+ if (separator === -1) {
42
+ throw new Error('Invalid terminal authorization message field');
43
+ }
44
+ const key = line.slice(0, separator).trim();
45
+ const value = line.slice(separator + 1).trim();
46
+ if (!key || !value || fields[key]) {
47
+ throw new Error('Invalid terminal authorization message field');
48
+ }
49
+ fields[key] = value;
50
+ }
51
+ for (const required of ['job', 'node', 'expiresAt']) {
52
+ if (!fields[required]) {
53
+ throw new Error(`Missing terminal authorization field: ${required}`);
54
+ }
55
+ }
56
+ // The required fields are present by the check above; the rest are optional.
57
+ return fields;
58
+ }
59
+ //# sourceMappingURL=terminalAuthorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminalAuthorization.js","sourceRoot":"","sources":["../src/terminalAuthorization.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,qCAAqC,GAAG,kCAAkC,CAAC;AACxF,MAAM,CAAC,MAAM,+BAA+B,GAAG,qBAAqB,CAAC;AACrE,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAoB/D,sGAAsG;AACtG,MAAM,UAAU,uBAAuB,CAAC,EAAU;IAChD,OAAO,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,iCAAiC,CAC/C,OAA4C;IAE5C,OAAO;QACL,qCAAqC;QACrC,EAAE;QACF,QAAQ,OAAO,CAAC,GAAG,EAAE;QACrB,SAAS,OAAO,CAAC,IAAI,EAAE;QACvB,cAAc,OAAO,CAAC,SAAS,EAAE;QACjC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,YAAY,OAAO,CAAC,OAAO,EAAE;QAC7B,aAAa,+BAA+B,EAAE;KAC/C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iCAAiC,CAC/C,OAAe;IAEf,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,qCAAqC,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/C,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAClE,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,KAAK,MAAM,QAAQ,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAU,EAAE,CAAC;QAC7D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,OAAO,MAAuD,CAAC;AACjE,CAAC"}
@@ -0,0 +1,60 @@
1
+ /** The job an SSH command reaches, and the node that runs it. */
2
+ export interface SshJobTarget {
3
+ job: string;
4
+ node: string;
5
+ /** The proxy domain of the node's network; the sidecar hostname hangs under it. */
6
+ nodeDomain: string;
7
+ }
8
+ export type SshProxyMode = 'socat' | 'nc';
9
+ /** How the local `ssh` client should reach the node's proxy. */
10
+ export interface SshCommandOptions {
11
+ /** Port of the node's CONNECT proxy; defaults to 5002. */
12
+ proxyPort?: string | number;
13
+ /** Which local tool tunnels through the proxy; defaults to `socat`. */
14
+ proxyMode?: SshProxyMode;
15
+ /** The private key to connect with, passed to `ssh -i`. */
16
+ identityFile?: string;
17
+ /**
18
+ * Which operation of the job to reach, as its position in the job
19
+ * definition's `ops`; every operation has its own SSH sidecar. Defaults to 0.
20
+ */
21
+ opIndex?: number;
22
+ }
23
+ export interface SshConnectionDescriptor {
24
+ executable: 'ssh';
25
+ username: string;
26
+ hostname: string;
27
+ proxyCommand: string;
28
+ args: string[];
29
+ /** The full command, shell-quoted for copy and paste. */
30
+ formattedCommand: string;
31
+ }
32
+ /** A source of cryptographically secure random bytes, such as Web Crypto. */
33
+ export interface SshCryptoProvider {
34
+ getRandomValues<T extends ArrayBufferView>(array: T): T;
35
+ }
36
+ export interface GenerateSshKeyPairOptions {
37
+ /** Trailing comment of the public key; defaults to `nosana`. */
38
+ comment?: string;
39
+ /** Defaults to `globalThis.crypto`. */
40
+ cryptoProvider?: SshCryptoProvider;
41
+ }
42
+ export interface SshKeyPair {
43
+ algorithm: 'ssh-ed25519';
44
+ /** One OpenSSH `authorized_keys` line. */
45
+ publicKey: string;
46
+ /** PEM-wrapped, unencrypted OpenSSH private key. */
47
+ privateKey: string;
48
+ }
49
+ export type SshPublicKeyParseErrorCode = 'INVALID_KEY' | 'TOO_MANY_KEYS' | 'KEY_SET_TOO_LARGE';
50
+ export interface SshPublicKeyParseError {
51
+ code: SshPublicKeyParseErrorCode;
52
+ /** 1-based line of the offending key; absent for whole-set errors. */
53
+ line?: number;
54
+ message: string;
55
+ }
56
+ export interface SshPublicKeyParseResult {
57
+ keys: string[];
58
+ errors: SshPublicKeyParseError[];
59
+ }
60
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,IAAI,CAAC;AAE1C,gEAAgE;AAChE,MAAM,WAAW,iBAAiB;IAChC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,uEAAuE;IACvE,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,KAAK,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,CAAC,SAAS,eAAe,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,yBAAyB;IACxC,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,cAAc,CAAC,EAAE,iBAAiB,CAAC;CACpC;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,aAAa,CAAC;IACzB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,0BAA0B,GAAG,aAAa,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAE/F,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,0BAA0B,CAAC;IACjC,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,sBAAsB,EAAE,CAAC;CAClC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@nosana/ssh",
3
+ "version": "2.13.1",
4
+ "description": "SSH tooling for Nosana jobs: OpenSSH key pairs, public-key validation, job definition keys and the ssh command that reaches a job",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "author": "Nosana",
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/nosana-ci/nosana-kit.git",
13
+ "directory": "packages/ssh"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc --build",
23
+ "build:with-deps": "pnpm --filter $npm_package_name... run build",
24
+ "dev": "tsc --watch",
25
+ "dev:with-deps": "pnpm --filter $npm_package_name... --parallel run dev",
26
+ "clean": "rm -rf dist",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "lint": "eslint ./src",
31
+ "lint:fix": "eslint --fix ./src",
32
+ "format": "prettier --config ../../.prettierrc --check \"src/**/*.ts\"",
33
+ "format:fix": "prettier --config ../../.prettierrc --write \"src/**/*.ts\""
34
+ },
35
+ "dependencies": {
36
+ "@nosana/types": "2.13.0",
37
+ "tweetnacl": "^1.0.3"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.0.14",
41
+ "@vitest/coverage-v8": "^4.0.16",
42
+ "prettier": "^3.7.4",
43
+ "typescript": "^5.9.3",
44
+ "vitest": "^4.0.16"
45
+ },
46
+ "packageManager": "pnpm@10.28.1",
47
+ "engines": {
48
+ "node": ">=20.18.0"
49
+ },
50
+ "gitHead": "694cadb116a2dcb5d27a591ceba5f91daaf39c73"
51
+ }