@jini-ai/platform 0.3.0 → 0.5.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/CHANGELOG.md +146 -0
- package/README.md +121 -2
- package/dist/asset-cache.d.ts +0 -22
- package/dist/asset-cache.js +3 -138
- package/dist/download.js +2 -2
- package/dist/fs/atomic-write.d.ts +103 -0
- package/dist/fs/atomic-write.js +201 -0
- package/dist/fs/durable-json.d.ts +89 -0
- package/dist/fs/durable-json.js +166 -0
- package/dist/fs/env-text.d.ts +11 -0
- package/dist/fs/env-text.js +22 -0
- package/dist/fs/file-lock.d.ts +126 -0
- package/dist/fs/file-lock.js +306 -0
- package/dist/fs/guarded-reader.d.ts +60 -0
- package/dist/fs/guarded-reader.js +310 -0
- package/dist/fs/index.d.ts +8 -0
- package/dist/fs/index.js +9 -0
- package/dist/fs/strict-containment.d.ts +5 -0
- package/dist/fs/strict-containment.js +13 -0
- package/dist/fs.js +2 -3
- package/dist/http/guarded/client.d.ts +13 -0
- package/dist/http/guarded/client.js +240 -0
- package/dist/http/guarded/errors.d.ts +21 -0
- package/dist/http/guarded/errors.js +20 -0
- package/dist/http/guarded/fetch-adapter.d.ts +23 -0
- package/dist/http/guarded/fetch-adapter.js +30 -0
- package/dist/http/guarded/index.d.ts +12 -0
- package/dist/http/guarded/index.js +7 -0
- package/dist/http/guarded/node-ports.d.ts +8 -0
- package/dist/http/guarded/node-ports.js +11 -0
- package/dist/http/guarded/ports.d.ts +51 -0
- package/dist/http/guarded/ports.js +2 -0
- package/dist/http/guarded/transport.fetch.d.ts +11 -0
- package/dist/http/guarded/transport.fetch.js +109 -0
- package/dist/http/guarded/types.d.ts +8 -0
- package/dist/http/guarded/types.js +2 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +61 -1
- package/dist/mail/adapter-module.d.ts +20 -0
- package/dist/mail/adapter-module.js +2 -0
- package/dist/mail/delivery-availability.d.ts +7 -0
- package/dist/mail/delivery-availability.js +6 -0
- package/dist/mail/index.d.ts +7 -0
- package/dist/mail/index.js +3 -0
- package/dist/mail/ports.d.ts +43 -0
- package/dist/mail/ports.js +2 -0
- package/dist/mail/purpose-scoped-mailer.d.ts +18 -0
- package/dist/mail/purpose-scoped-mailer.js +50 -0
- package/dist/mail/smtp.d.ts +79 -0
- package/dist/mail/smtp.js +102 -0
- package/dist/mail/types.d.ts +83 -0
- package/dist/mail/types.js +2 -0
- package/dist/messages.d.ts +22 -0
- package/dist/messages.js +15 -0
- package/dist/net/address.d.ts +38 -0
- package/dist/net/address.js +241 -0
- package/dist/net/index.d.ts +2 -0
- package/dist/net/index.js +2 -0
- package/dist/process.d.ts +2 -1
- package/dist/process.js +2 -1
- package/dist/proxy-env.js +1 -1
- package/dist/resource-paths.js +1 -1
- package/dist/sandbox-env.d.ts +1 -1
- package/dist/sandbox-env.js +1 -1
- package/dist/secrets/aad.d.ts +7 -0
- package/dist/secrets/aad.js +5 -0
- package/dist/secrets/credential-sets/aad.d.ts +7 -0
- package/dist/secrets/credential-sets/aad.js +7 -0
- package/dist/secrets/credential-sets/index.d.ts +4 -0
- package/dist/secrets/credential-sets/index.js +3 -0
- package/dist/secrets/credential-sets/repo.memory.d.ts +36 -0
- package/dist/secrets/credential-sets/repo.memory.js +72 -0
- package/dist/secrets/credential-sets/types.d.ts +67 -0
- package/dist/secrets/credential-sets/types.js +6 -0
- package/dist/secrets/index.d.ts +9 -0
- package/dist/secrets/index.js +5 -0
- package/dist/secrets/keyring.env.d.ts +176 -0
- package/dist/secrets/keyring.env.js +271 -0
- package/dist/secrets/keyring.memory.d.ts +10 -0
- package/dist/secrets/keyring.memory.js +9 -0
- package/dist/secrets/ports.d.ts +33 -0
- package/dist/secrets/ports.js +2 -0
- package/dist/secrets/secret-sealer.aesgcm.d.ts +22 -0
- package/dist/secrets/secret-sealer.aesgcm.js +64 -0
- package/dist/secrets/testing.d.ts +4 -0
- package/dist/secrets/testing.js +4 -0
- package/dist/secrets/types.d.ts +10 -0
- package/dist/secrets/types.js +2 -0
- package/dist/shell.d.ts +10 -6
- package/dist/shell.js +10 -6
- package/dist/terminal.d.ts +2 -2
- package/dist/terminal.js +9 -5
- package/package.json +79 -4
- package/dist/asset-cache.d.ts.map +0 -1
- package/dist/asset-cache.js.map +0 -1
- package/dist/aws-sigv4.d.ts.map +0 -1
- package/dist/aws-sigv4.js.map +0 -1
- package/dist/blob-storage.d.ts.map +0 -1
- package/dist/blob-storage.js.map +0 -1
- package/dist/command.d.ts.map +0 -1
- package/dist/command.js.map +0 -1
- package/dist/download.d.ts.map +0 -1
- package/dist/download.js.map +0 -1
- package/dist/fetch-with-timeout.d.ts.map +0 -1
- package/dist/fetch-with-timeout.js.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/home-expansion.d.ts.map +0 -1
- package/dist/home-expansion.js.map +0 -1
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/process.d.ts.map +0 -1
- package/dist/process.js.map +0 -1
- package/dist/proxy-env.d.ts.map +0 -1
- package/dist/proxy-env.js.map +0 -1
- package/dist/resource-paths.d.ts.map +0 -1
- package/dist/resource-paths.js.map +0 -1
- package/dist/sandbox-env.d.ts.map +0 -1
- package/dist/sandbox-env.js.map +0 -1
- package/dist/shell.d.ts.map +0 -1
- package/dist/shell.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/toolchain.d.ts.map +0 -1
- package/dist/toolchain.js.map +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { type PlatformMessages } from "../messages.js";
|
|
2
|
+
import type { KeyringPort } from "./ports.js";
|
|
3
|
+
/** Environment access belongs to the host; the library never reads ambient process state. */
|
|
4
|
+
export interface RootKeyEnvironmentPort {
|
|
5
|
+
read(required: {
|
|
6
|
+
name: string;
|
|
7
|
+
}): string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Host configuration has no implicit env name, file path, permissions, or HKDF salt. */
|
|
10
|
+
export interface EnvOrFileKeyringRequired {
|
|
11
|
+
/** Live environment reader; key material is resolved and cached on first derivation. */
|
|
12
|
+
env: RootKeyEnvironmentPort;
|
|
13
|
+
/** Explicit permission to read the configured file when env material is absent. */
|
|
14
|
+
allowFileFallback: boolean;
|
|
15
|
+
/** Explicit permission to create a missing file; fallback must also be enabled. */
|
|
16
|
+
allowFileAutoGenerate: boolean;
|
|
17
|
+
/** Wire constant supplied by the host; changing it changes every derived key. */
|
|
18
|
+
hkdfSalt: string | Uint8Array;
|
|
19
|
+
/** Name of an env var containing at least 32 bytes of hex-encoded key material. */
|
|
20
|
+
envVarName: string;
|
|
21
|
+
/** Explicit path to the file fallback. No runtime-mode or home-directory policy. */
|
|
22
|
+
keyFilePath: string;
|
|
23
|
+
}
|
|
24
|
+
export interface EnvOrFileKeyringOptions {
|
|
25
|
+
messages?: PlatformMessages;
|
|
26
|
+
/** Root-key generation identity; defaults to "v1". */
|
|
27
|
+
keyId?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Resolves injected env first, then explicitly permitted files; caches the root per instance.
|
|
30
|
+
* Missing source permissions throw TypeError at construction, before file access.
|
|
31
|
+
* Derivation rejects absent/invalid material; only explicit fallback plus generation writes a file.
|
|
32
|
+
* @complexity Construction is O(1); first derivation parses O(k) key characters and caches them.
|
|
33
|
+
* @example new EnvOrFileKeyring({ env, envVarName, keyFilePath, hkdfSalt, allowFileFallback: false, allowFileAutoGenerate: false })
|
|
34
|
+
*/
|
|
35
|
+
export declare class EnvOrFileKeyring implements KeyringPort {
|
|
36
|
+
private readonly messages;
|
|
37
|
+
private readonly env;
|
|
38
|
+
private readonly envVarName;
|
|
39
|
+
private readonly keyFilePath;
|
|
40
|
+
private readonly keyId;
|
|
41
|
+
private readonly allowFileFallback;
|
|
42
|
+
private readonly allowFileAutoGenerate;
|
|
43
|
+
private readonly hkdfSalt;
|
|
44
|
+
private cachedRootKey;
|
|
45
|
+
constructor(required: EnvOrFileKeyringRequired, optional?: EnvOrFileKeyringOptions);
|
|
46
|
+
activeKey(_required: Record<string, never>): Promise<{
|
|
47
|
+
readonly keyId: string;
|
|
48
|
+
}>;
|
|
49
|
+
deriveSigningSecret(input: {
|
|
50
|
+
workspaceId: string;
|
|
51
|
+
subscriptionId: string;
|
|
52
|
+
version: number;
|
|
53
|
+
}): Promise<Uint8Array>;
|
|
54
|
+
derive(input: {
|
|
55
|
+
workspaceId: string;
|
|
56
|
+
purpose: string;
|
|
57
|
+
info: string;
|
|
58
|
+
}): Promise<Uint8Array>;
|
|
59
|
+
private resolveRootKey;
|
|
60
|
+
private parseEnvRootKey;
|
|
61
|
+
private parseFileRootKey;
|
|
62
|
+
}
|
|
63
|
+
/** SHA-256 HKDF, 32 bytes, with info `${workspaceId}:${subscriptionId}:v${version}`. */
|
|
64
|
+
export declare function deriveSigningSecretFromRootKey({ rootKey, input, hkdfSalt }: {
|
|
65
|
+
rootKey: Uint8Array;
|
|
66
|
+
input: {
|
|
67
|
+
workspaceId: string;
|
|
68
|
+
subscriptionId: string;
|
|
69
|
+
version: number;
|
|
70
|
+
};
|
|
71
|
+
hkdfSalt: string | Uint8Array;
|
|
72
|
+
}): Uint8Array;
|
|
73
|
+
/** SHA-256 HKDF, 32 bytes, with info `${purpose}:${workspaceId}:${info}`. */
|
|
74
|
+
export declare function deriveFromRootKey({ rootKey, input, hkdfSalt }: {
|
|
75
|
+
rootKey: Uint8Array;
|
|
76
|
+
input: {
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
purpose: string;
|
|
79
|
+
info: string;
|
|
80
|
+
};
|
|
81
|
+
hkdfSalt: string | Uint8Array;
|
|
82
|
+
}): Uint8Array;
|
|
83
|
+
/** Derives from a validated candidate key without reading or writing any host source. */
|
|
84
|
+
export declare class FixedRootKeyKeyring implements KeyringPort {
|
|
85
|
+
private readonly rootKey;
|
|
86
|
+
private readonly keyId;
|
|
87
|
+
private readonly hkdfSalt;
|
|
88
|
+
constructor(required: {
|
|
89
|
+
hex: string;
|
|
90
|
+
hkdfSalt: string | Uint8Array;
|
|
91
|
+
}, optional?: {
|
|
92
|
+
keyId?: string;
|
|
93
|
+
});
|
|
94
|
+
activeKey(_required: Record<string, never>): Promise<{
|
|
95
|
+
readonly keyId: string;
|
|
96
|
+
}>;
|
|
97
|
+
deriveSigningSecret(input: {
|
|
98
|
+
workspaceId: string;
|
|
99
|
+
subscriptionId: string;
|
|
100
|
+
version: number;
|
|
101
|
+
}): Promise<Uint8Array>;
|
|
102
|
+
derive(input: {
|
|
103
|
+
workspaceId: string;
|
|
104
|
+
purpose: string;
|
|
105
|
+
info: string;
|
|
106
|
+
}): Promise<Uint8Array>;
|
|
107
|
+
}
|
|
108
|
+
export type RootKeyRejection = "empty" | "not-hex" | "odd-length" | "too-short";
|
|
109
|
+
export type ParsedRootKeyHex = {
|
|
110
|
+
readonly ok: true;
|
|
111
|
+
readonly hex: string;
|
|
112
|
+
} | {
|
|
113
|
+
readonly ok: false;
|
|
114
|
+
readonly reason: RootKeyRejection;
|
|
115
|
+
readonly hexDigits: number;
|
|
116
|
+
};
|
|
117
|
+
/** Validates trimmed hex of at least 32 bytes without disclosing rejected material. */
|
|
118
|
+
export declare function parseRootKeyHex({ raw }: {
|
|
119
|
+
raw: string;
|
|
120
|
+
}): ParsedRootKeyHex;
|
|
121
|
+
export declare class UnusableRootKeyError extends Error {
|
|
122
|
+
readonly source: "env" | "file";
|
|
123
|
+
readonly reason: RootKeyRejection;
|
|
124
|
+
constructor(input: {
|
|
125
|
+
source: "env" | "file";
|
|
126
|
+
reason: RootKeyRejection;
|
|
127
|
+
message: string;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** SHA-256 of decoded bytes, truncated to 12 hex digits; never a usable secret. */
|
|
131
|
+
export declare function fingerprintRootKeyHex({ hex }: {
|
|
132
|
+
hex: string;
|
|
133
|
+
}): string;
|
|
134
|
+
export interface RootKeyStatus {
|
|
135
|
+
readonly active: boolean;
|
|
136
|
+
readonly source: "env" | "file" | "none";
|
|
137
|
+
/** Present only when usable key material exists. */
|
|
138
|
+
readonly fingerprint?: string;
|
|
139
|
+
/** A present source failed validation; this differs from an absent source. */
|
|
140
|
+
readonly invalid?: boolean;
|
|
141
|
+
readonly reason?: RootKeyRejection;
|
|
142
|
+
readonly keyFilePath: string;
|
|
143
|
+
}
|
|
144
|
+
export interface InspectRootKeyMaterialOptions {
|
|
145
|
+
env: RootKeyEnvironmentPort;
|
|
146
|
+
envVarName: string;
|
|
147
|
+
keyFilePath: string;
|
|
148
|
+
}
|
|
149
|
+
/** Fresh injected env/file status read, without caching, writes or exposing key material.
|
|
150
|
+
* @complexity O(k) time/space for k key characters; at most one file read.
|
|
151
|
+
* @example inspectRootKeyMaterial({ env, envVarName, keyFilePath })
|
|
152
|
+
*/
|
|
153
|
+
export declare function inspectRootKeyMaterial(options: InspectRootKeyMaterialOptions): RootKeyStatus;
|
|
154
|
+
export interface RootKeyReveal extends RootKeyStatus {
|
|
155
|
+
readonly hex?: string;
|
|
156
|
+
}
|
|
157
|
+
/** Explicit sensitive read through host env/file sources; returns usable key material in the clear.
|
|
158
|
+
* @complexity O(k) time/space for k key characters; at most one file read and no writes.
|
|
159
|
+
* @example revealRootKeyMaterial({ env, envVarName, keyFilePath })
|
|
160
|
+
*/
|
|
161
|
+
export declare function revealRootKeyMaterial(options: InspectRootKeyMaterialOptions): RootKeyReveal;
|
|
162
|
+
export declare class RootKeyFileAlreadyExistsError extends Error {
|
|
163
|
+
constructor({ keyFilePath }: {
|
|
164
|
+
keyFilePath: string;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export interface GeneratedFileRootKey {
|
|
168
|
+
readonly hex: string;
|
|
169
|
+
readonly fingerprint: string;
|
|
170
|
+
readonly keyFilePath: string;
|
|
171
|
+
}
|
|
172
|
+
/** Creates a 32-byte root key at mode 0600; throws RootKeyFileAlreadyExistsError on collisions. */
|
|
173
|
+
export declare function generateFileRootKey(options: {
|
|
174
|
+
keyFilePath: string;
|
|
175
|
+
}): GeneratedFileRootKey;
|
|
176
|
+
//# sourceMappingURL=keyring.env.d.ts.map
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { defaultPlatformMessages } from "../messages.js";
|
|
2
|
+
/** Env/file root-key adapter and uncached inspection helpers. Raw keys never enter sealed records.
|
|
3
|
+
* HKDF salts and source locations belong to the host. File generation refuses existing paths.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash, hkdfSync, randomBytes } from "node:crypto";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
const DERIVED_SECRET_LENGTH_BYTES = 32;
|
|
9
|
+
const ROOT_KEY_LENGTH_BYTES = 32;
|
|
10
|
+
/** Resolves injected env first, then explicitly permitted files; caches the root per instance.
|
|
11
|
+
* Missing source permissions throw TypeError at construction, before file access.
|
|
12
|
+
* Derivation rejects absent/invalid material; only explicit fallback plus generation writes a file.
|
|
13
|
+
* @complexity Construction is O(1); first derivation parses O(k) key characters and caches them.
|
|
14
|
+
* @example new EnvOrFileKeyring({ env, envVarName, keyFilePath, hkdfSalt, allowFileFallback: false, allowFileAutoGenerate: false })
|
|
15
|
+
*/
|
|
16
|
+
export class EnvOrFileKeyring {
|
|
17
|
+
messages;
|
|
18
|
+
env;
|
|
19
|
+
envVarName;
|
|
20
|
+
keyFilePath;
|
|
21
|
+
keyId;
|
|
22
|
+
allowFileFallback;
|
|
23
|
+
allowFileAutoGenerate;
|
|
24
|
+
hkdfSalt;
|
|
25
|
+
cachedRootKey;
|
|
26
|
+
constructor(required, optional = {}) {
|
|
27
|
+
requireHostOption(required?.envVarName, "envVarName");
|
|
28
|
+
requireHostOption(required?.keyFilePath, "keyFilePath");
|
|
29
|
+
requireHostOption(required?.hkdfSalt, "hkdfSalt");
|
|
30
|
+
if (typeof required.env?.read !== "function")
|
|
31
|
+
throw new TypeError("env.read is required");
|
|
32
|
+
if (typeof required.allowFileFallback !== "boolean")
|
|
33
|
+
throw new TypeError("allowFileFallback must be an explicit boolean");
|
|
34
|
+
if (typeof required.allowFileAutoGenerate !== "boolean")
|
|
35
|
+
throw new TypeError("allowFileAutoGenerate must be an explicit boolean");
|
|
36
|
+
this.messages = optional.messages ?? defaultPlatformMessages;
|
|
37
|
+
this.env = required.env;
|
|
38
|
+
this.hkdfSalt = required.hkdfSalt;
|
|
39
|
+
this.envVarName = required.envVarName;
|
|
40
|
+
this.keyFilePath = required.keyFilePath;
|
|
41
|
+
this.keyId = optional.keyId ?? "v1";
|
|
42
|
+
this.allowFileFallback = required.allowFileFallback;
|
|
43
|
+
this.allowFileAutoGenerate = required.allowFileAutoGenerate;
|
|
44
|
+
}
|
|
45
|
+
async activeKey(_required) {
|
|
46
|
+
return { keyId: this.keyId };
|
|
47
|
+
}
|
|
48
|
+
async deriveSigningSecret(input) {
|
|
49
|
+
return deriveSigningSecretFromRootKey({ rootKey: this.resolveRootKey(), input, hkdfSalt: this.hkdfSalt });
|
|
50
|
+
}
|
|
51
|
+
async derive(input) {
|
|
52
|
+
return deriveFromRootKey({ rootKey: this.resolveRootKey(), input, hkdfSalt: this.hkdfSalt });
|
|
53
|
+
}
|
|
54
|
+
resolveRootKey() {
|
|
55
|
+
// Validate env and file through the same parser used by inspection; a status screen and
|
|
56
|
+
// the sealer must never disagree about whether configured material is a usable root key.
|
|
57
|
+
// Buffer.from(raw, 'hex') alone can turn malformed text into empty bytes that HKDF accepts,
|
|
58
|
+
// yielding keys computable from public constants rather than a real root secret.
|
|
59
|
+
if (this.cachedRootKey)
|
|
60
|
+
return this.cachedRootKey;
|
|
61
|
+
const fromEnv = this.env.read({ name: this.envVarName });
|
|
62
|
+
if (fromEnv) {
|
|
63
|
+
this.cachedRootKey = this.parseEnvRootKey(fromEnv);
|
|
64
|
+
return this.cachedRootKey;
|
|
65
|
+
}
|
|
66
|
+
if (!this.allowFileFallback) {
|
|
67
|
+
throw new Error(`no root key: ${this.envVarName} is not set and allowFileFallback is disabled`);
|
|
68
|
+
}
|
|
69
|
+
if (existsSync(this.keyFilePath)) {
|
|
70
|
+
this.cachedRootKey = this.parseFileRootKey(readFileSync(this.keyFilePath, "utf8"));
|
|
71
|
+
return this.cachedRootKey;
|
|
72
|
+
}
|
|
73
|
+
if (!this.allowFileAutoGenerate) {
|
|
74
|
+
throw new Error(`no root key: ${this.envVarName} is not set, no key file exists at ${this.keyFilePath}, and this instance does not auto-generate one — set the env var, or call generateFileRootKey() explicitly`);
|
|
75
|
+
}
|
|
76
|
+
const generated = randomBytes(ROOT_KEY_LENGTH_BYTES);
|
|
77
|
+
mkdirSync(dirname(this.keyFilePath), { recursive: true });
|
|
78
|
+
writeFileSync(this.keyFilePath, generated.toString("hex"), { mode: 0o600 });
|
|
79
|
+
this.cachedRootKey = generated;
|
|
80
|
+
return this.cachedRootKey;
|
|
81
|
+
}
|
|
82
|
+
parseEnvRootKey(raw) {
|
|
83
|
+
// Legacy env parsing rejected malformed hex but could accept short material: only that
|
|
84
|
+
// rejection needs the warning that existing credentials may already use these bytes.
|
|
85
|
+
const parsed = parseRootKeyHex({ raw });
|
|
86
|
+
if (parsed.ok)
|
|
87
|
+
return Buffer.from(parsed.hex, "hex");
|
|
88
|
+
throw new UnusableRootKeyError({
|
|
89
|
+
source: "env",
|
|
90
|
+
reason: parsed.reason,
|
|
91
|
+
message: unusableRootKeyMessage({
|
|
92
|
+
messages: this.messages,
|
|
93
|
+
subject: this.envVarName,
|
|
94
|
+
detail: describeRootKeyRejection(parsed),
|
|
95
|
+
sealedWarningSubject: parsed.reason === "too-short" ? "this value" : undefined,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
parseFileRootKey(raw) {
|
|
100
|
+
// Legacy file decoding accepted arbitrary content, so every rejected file may have been
|
|
101
|
+
// used to seal data. Never rewrite or repair it here: replacing bytes orphans those rows.
|
|
102
|
+
const parsed = parseRootKeyHex({ raw });
|
|
103
|
+
if (parsed.ok)
|
|
104
|
+
return Buffer.from(parsed.hex, "hex");
|
|
105
|
+
throw new UnusableRootKeyError({
|
|
106
|
+
source: "file",
|
|
107
|
+
reason: parsed.reason,
|
|
108
|
+
message: unusableRootKeyMessage({
|
|
109
|
+
messages: this.messages,
|
|
110
|
+
subject: `the root key file at ${this.keyFilePath}`,
|
|
111
|
+
detail: describeRootKeyRejection(parsed),
|
|
112
|
+
sealedWarningSubject: "this file",
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** SHA-256 HKDF, 32 bytes, with info `${workspaceId}:${subscriptionId}:v${version}`. */
|
|
118
|
+
// Both installed and candidate keyrings use this derivation so checking a candidate tests the
|
|
119
|
+
// exact secret that persisted root material would produce, not a parallel approximation.
|
|
120
|
+
export function deriveSigningSecretFromRootKey({ rootKey, input, hkdfSalt }) {
|
|
121
|
+
const info = `${input.workspaceId}:${input.subscriptionId}:v${input.version}`;
|
|
122
|
+
return new Uint8Array(hkdfSync("sha256", rootKey, hkdfSalt, info, DERIVED_SECRET_LENGTH_BYTES));
|
|
123
|
+
}
|
|
124
|
+
/** SHA-256 HKDF, 32 bytes, with info `${purpose}:${workspaceId}:${info}`. */
|
|
125
|
+
// purpose is part of HKDF info, not display metadata: it separates generic uses from signing secrets.
|
|
126
|
+
export function deriveFromRootKey({ rootKey, input, hkdfSalt }) {
|
|
127
|
+
const effectiveInfo = `${input.purpose}:${input.workspaceId}:${input.info}`;
|
|
128
|
+
return new Uint8Array(hkdfSync("sha256", rootKey, hkdfSalt, effectiveInfo, DERIVED_SECRET_LENGTH_BYTES));
|
|
129
|
+
}
|
|
130
|
+
/** Derives from a validated candidate key without reading or writing any host source. */
|
|
131
|
+
export class FixedRootKeyKeyring {
|
|
132
|
+
rootKey;
|
|
133
|
+
keyId;
|
|
134
|
+
hkdfSalt;
|
|
135
|
+
constructor(required, optional = {}) {
|
|
136
|
+
requireHostOption(required?.hkdfSalt, "hkdfSalt");
|
|
137
|
+
this.hkdfSalt = required.hkdfSalt;
|
|
138
|
+
this.keyId = optional.keyId ?? "v1";
|
|
139
|
+
const parsed = parseRootKeyHex({ raw: required.hex });
|
|
140
|
+
if (!parsed.ok)
|
|
141
|
+
throw new Error(`FixedRootKeyKeyring: not a valid root key (${parsed.reason})`);
|
|
142
|
+
this.rootKey = Buffer.from(parsed.hex, "hex");
|
|
143
|
+
}
|
|
144
|
+
async activeKey(_required) {
|
|
145
|
+
return { keyId: this.keyId };
|
|
146
|
+
}
|
|
147
|
+
async deriveSigningSecret(input) {
|
|
148
|
+
return deriveSigningSecretFromRootKey({ rootKey: this.rootKey, input, hkdfSalt: this.hkdfSalt });
|
|
149
|
+
}
|
|
150
|
+
async derive(input) {
|
|
151
|
+
return deriveFromRootKey({ rootKey: this.rootKey, input, hkdfSalt: this.hkdfSalt });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const HEX_KEY_PATTERN = /^[0-9a-f]+$/i;
|
|
155
|
+
/** Validates trimmed hex of at least 32 bytes without disclosing rejected material. */
|
|
156
|
+
export function parseRootKeyHex({ raw }) {
|
|
157
|
+
const hex = raw.trim();
|
|
158
|
+
if (hex.length === 0)
|
|
159
|
+
return { ok: false, reason: "empty", hexDigits: 0 };
|
|
160
|
+
if (!HEX_KEY_PATTERN.test(hex))
|
|
161
|
+
return { ok: false, reason: "not-hex", hexDigits: hex.length };
|
|
162
|
+
if (hex.length % 2 !== 0)
|
|
163
|
+
return { ok: false, reason: "odd-length", hexDigits: hex.length };
|
|
164
|
+
if (hex.length < ROOT_KEY_LENGTH_BYTES * 2)
|
|
165
|
+
return { ok: false, reason: "too-short", hexDigits: hex.length };
|
|
166
|
+
return { ok: true, hex };
|
|
167
|
+
}
|
|
168
|
+
const ROOT_KEY_REJECTION_DETAIL = {
|
|
169
|
+
empty: () => "it is empty",
|
|
170
|
+
"not-hex": () => 'it contains characters that are not hex digits (only 0-9 and a-f are allowed; a "0x" prefix, whitespace inside the value, or a base64 or PEM body all fail this)',
|
|
171
|
+
"odd-length": (hexDigits) => `it has an odd number of hex digits (${hexDigits}), so it does not describe whole bytes — usually a partial write or a truncated copy`,
|
|
172
|
+
"too-short": (hexDigits) => `it is ${hexDigits / 2} bytes (${hexDigits} hex digits); a root key must be at least ${ROOT_KEY_LENGTH_BYTES} bytes (${ROOT_KEY_LENGTH_BYTES * 2} hex digits) — usually a truncated copy`,
|
|
173
|
+
};
|
|
174
|
+
function describeRootKeyRejection(parsed) {
|
|
175
|
+
return ROOT_KEY_REJECTION_DETAIL[parsed.reason](parsed.hexDigits);
|
|
176
|
+
}
|
|
177
|
+
function unusableRootKeyMessage(input) {
|
|
178
|
+
const refusal = `${input.subject} is not usable as a root key: ${input.detail}. The keyring refuses to derive any key material from it rather than sealing under a shortened or empty key.`;
|
|
179
|
+
if (input.sealedWarningSubject === undefined)
|
|
180
|
+
return refusal;
|
|
181
|
+
return (`${refusal} ${(input.messages ?? defaultPlatformMessages).rootKeySealedWarning({ subject: input.sealedWarningSubject })}` +
|
|
182
|
+
`not from a real ${ROOT_KEY_LENGTH_BYTES}-byte key, so those stored credentials are not protected as intended — with empty or very short material the derived key is computable from published constants. ` +
|
|
183
|
+
`Replacing or regenerating the key will NOT restore them; it will make them unreadable. ` +
|
|
184
|
+
`Do not overwrite, delete or regenerate this key until you have decided what to do with the credentials already stored.`);
|
|
185
|
+
}
|
|
186
|
+
export class UnusableRootKeyError extends Error {
|
|
187
|
+
source;
|
|
188
|
+
reason;
|
|
189
|
+
constructor(input) {
|
|
190
|
+
super(input.message);
|
|
191
|
+
this.name = "UnusableRootKeyError";
|
|
192
|
+
this.source = input.source;
|
|
193
|
+
this.reason = input.reason;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** SHA-256 of decoded bytes, truncated to 12 hex digits; never a usable secret. */
|
|
197
|
+
export function fingerprintRootKeyHex({ hex }) {
|
|
198
|
+
return createHash("sha256").update(Buffer.from(hex, "hex")).digest("hex").slice(0, 12);
|
|
199
|
+
}
|
|
200
|
+
function readActiveRootKeyMaterial(input) {
|
|
201
|
+
const fromEnv = input.env.read({ name: input.envVarName });
|
|
202
|
+
if (fromEnv)
|
|
203
|
+
return toActiveRootKeyMaterial("env", fromEnv);
|
|
204
|
+
if (existsSync(input.keyFilePath))
|
|
205
|
+
return toActiveRootKeyMaterial("file", readFileSync(input.keyFilePath, "utf8"));
|
|
206
|
+
return { source: "none" };
|
|
207
|
+
}
|
|
208
|
+
function toActiveRootKeyMaterial(source, raw) {
|
|
209
|
+
const parsed = parseRootKeyHex({ raw });
|
|
210
|
+
return parsed.ok ? { source, hex: parsed.hex } : { source, invalid: true, reason: parsed.reason };
|
|
211
|
+
}
|
|
212
|
+
/** Fresh injected env/file status read, without caching, writes or exposing key material.
|
|
213
|
+
* @complexity O(k) time/space for k key characters; at most one file read.
|
|
214
|
+
* @example inspectRootKeyMaterial({ env, envVarName, keyFilePath })
|
|
215
|
+
*/
|
|
216
|
+
export function inspectRootKeyMaterial(options) {
|
|
217
|
+
const envVarName = options.envVarName;
|
|
218
|
+
const keyFilePath = options.keyFilePath;
|
|
219
|
+
const raw = readActiveRootKeyMaterial({ env: options.env, envVarName, keyFilePath });
|
|
220
|
+
if (raw.hex)
|
|
221
|
+
return { active: true, source: raw.source, fingerprint: fingerprintRootKeyHex({ hex: raw.hex }), keyFilePath };
|
|
222
|
+
return { active: false, source: raw.source, ...invalidFields(raw), keyFilePath };
|
|
223
|
+
}
|
|
224
|
+
function invalidFields(raw) {
|
|
225
|
+
return raw.invalid ? { invalid: true, reason: raw.reason } : {};
|
|
226
|
+
}
|
|
227
|
+
/** Explicit sensitive read through host env/file sources; returns usable key material in the clear.
|
|
228
|
+
* @complexity O(k) time/space for k key characters; at most one file read and no writes.
|
|
229
|
+
* @example revealRootKeyMaterial({ env, envVarName, keyFilePath })
|
|
230
|
+
*/
|
|
231
|
+
export function revealRootKeyMaterial(options) {
|
|
232
|
+
const envVarName = options.envVarName;
|
|
233
|
+
const keyFilePath = options.keyFilePath;
|
|
234
|
+
const raw = readActiveRootKeyMaterial({ env: options.env, envVarName, keyFilePath });
|
|
235
|
+
if (raw.hex)
|
|
236
|
+
return { active: true, source: raw.source, hex: raw.hex, fingerprint: fingerprintRootKeyHex({ hex: raw.hex }), keyFilePath };
|
|
237
|
+
return { active: false, source: raw.source, ...invalidFields(raw), keyFilePath };
|
|
238
|
+
}
|
|
239
|
+
export class RootKeyFileAlreadyExistsError extends Error {
|
|
240
|
+
constructor({ keyFilePath }) {
|
|
241
|
+
super(`a root key file already exists at ${keyFilePath} — generate never overwrites an existing key`);
|
|
242
|
+
this.name = "RootKeyFileAlreadyExistsError";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Creates a 32-byte root key at mode 0600; throws RootKeyFileAlreadyExistsError on collisions. */
|
|
246
|
+
export function generateFileRootKey(options) {
|
|
247
|
+
const keyFilePath = options.keyFilePath;
|
|
248
|
+
const generated = randomBytes(ROOT_KEY_LENGTH_BYTES);
|
|
249
|
+
const hex = generated.toString("hex");
|
|
250
|
+
mkdirSync(dirname(keyFilePath), { recursive: true, mode: 0o700 });
|
|
251
|
+
try {
|
|
252
|
+
writeFileSync(keyFilePath, hex, { mode: 0o600, flag: "wx" });
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (isAlreadyExistsError(err))
|
|
256
|
+
throw new RootKeyFileAlreadyExistsError({ keyFilePath });
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
return { hex, fingerprint: fingerprintRootKeyHex({ hex }), keyFilePath };
|
|
260
|
+
}
|
|
261
|
+
function isAlreadyExistsError(err) {
|
|
262
|
+
if (typeof err !== "object" || err === null || !("code" in err))
|
|
263
|
+
return false;
|
|
264
|
+
const code = err.code;
|
|
265
|
+
return code === "EEXIST" || code === "ELOOP";
|
|
266
|
+
}
|
|
267
|
+
function requireHostOption(value, name) {
|
|
268
|
+
if (value === undefined || value === null)
|
|
269
|
+
throw new TypeError(`${name} is required`);
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=keyring.env.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FixedRootKeyKeyring } from "./keyring.env.js";
|
|
2
|
+
/** Real HKDF over an ephemeral 32-byte key, with no environment or filesystem resolution. */
|
|
3
|
+
export declare class InMemoryKeyring extends FixedRootKeyKeyring {
|
|
4
|
+
constructor(required: {
|
|
5
|
+
hkdfSalt: string | Uint8Array;
|
|
6
|
+
}, optional?: {
|
|
7
|
+
keyId?: string;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=keyring.memory.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { FixedRootKeyKeyring } from "./keyring.env.js";
|
|
3
|
+
/** Real HKDF over an ephemeral 32-byte key, with no environment or filesystem resolution. */
|
|
4
|
+
export class InMemoryKeyring extends FixedRootKeyKeyring {
|
|
5
|
+
constructor(required, optional = {}) {
|
|
6
|
+
super({ hex: randomBytes(32).toString("hex"), hkdfSalt: required.hkdfSalt }, optional);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=keyring.memory.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SealedSecret } from "./types.js";
|
|
2
|
+
/** Generation identity only: a sealed record never carries root-key bytes. */
|
|
3
|
+
export interface RootKeyHandle {
|
|
4
|
+
readonly keyId: string;
|
|
5
|
+
}
|
|
6
|
+
/** Swappable root-key adapter with two stable HKDF info layouts. */
|
|
7
|
+
export interface KeyringPort {
|
|
8
|
+
activeKey(_required: Record<string, never>): Promise<RootKeyHandle>;
|
|
9
|
+
deriveSigningSecret(input: {
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
subscriptionId: string;
|
|
12
|
+
version: number;
|
|
13
|
+
}): Promise<Uint8Array>;
|
|
14
|
+
derive(input: {
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
purpose: string;
|
|
17
|
+
info: string;
|
|
18
|
+
}): Promise<Uint8Array>;
|
|
19
|
+
}
|
|
20
|
+
/** AAD binds new ciphertext to its context; absent open AAD supports historical records only. */
|
|
21
|
+
export interface SecretSealerPort {
|
|
22
|
+
seal(input: {
|
|
23
|
+
plaintext: string;
|
|
24
|
+
key: RootKeyHandle;
|
|
25
|
+
aad: string;
|
|
26
|
+
}): Promise<SealedSecret>;
|
|
27
|
+
open(input: {
|
|
28
|
+
sealed: SealedSecret;
|
|
29
|
+
}, optional?: {
|
|
30
|
+
aad?: string;
|
|
31
|
+
}): Promise<string>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { KeyringPort, RootKeyHandle, SecretSealerPort } from "./ports.js";
|
|
2
|
+
import type { SealedSecret } from "./types.js";
|
|
3
|
+
export declare class AesGcmSecretSealer implements SecretSealerPort {
|
|
4
|
+
private readonly keyring;
|
|
5
|
+
constructor({ keyring }: {
|
|
6
|
+
keyring: KeyringPort;
|
|
7
|
+
});
|
|
8
|
+
/** Encrypts under the requested generation with a fresh IV; AAD must be reconstructed on open. */
|
|
9
|
+
seal(input: {
|
|
10
|
+
plaintext: string;
|
|
11
|
+
key: RootKeyHandle;
|
|
12
|
+
aad: string;
|
|
13
|
+
}): Promise<SealedSecret>;
|
|
14
|
+
/** Opens under the envelope's generation. Wrong key, tag, nonce, or AAD fails authentication. */
|
|
15
|
+
open(input: {
|
|
16
|
+
sealed: SealedSecret;
|
|
17
|
+
}, { aad }?: {
|
|
18
|
+
aad?: string;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
private deriveAesKey;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=secret-sealer.aesgcm.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** AES-256-GCM with a fresh 12-byte IV and ciphertext || 16-byte tag, both base64 encoded.
|
|
2
|
+
* Uses purpose "secret-sealer.v1" and scope "secret-sealer" for HKDF domain separation.
|
|
3
|
+
* Seal requires context AAD; open accepts absent AAD solely for historical unbound records.
|
|
4
|
+
*/
|
|
5
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
6
|
+
const ALG = "aes-256-gcm";
|
|
7
|
+
const IV_LENGTH_BYTES = 12;
|
|
8
|
+
const AUTH_TAG_LENGTH_BYTES = 16;
|
|
9
|
+
const SEALER_PURPOSE = "secret-sealer.v1";
|
|
10
|
+
const SEALER_KEY_SCOPE = "secret-sealer";
|
|
11
|
+
export class AesGcmSecretSealer {
|
|
12
|
+
// Derive one key per root generation, not per tenant. Data-layer ownership checks and context AAD
|
|
13
|
+
// must prevent ciphertext substitution between rows; the shared encryption key alone cannot.
|
|
14
|
+
// Root keys stay behind handles, and the purpose separates this HKDF domain from signing/tokens.
|
|
15
|
+
// Exercise real GCM with an in-memory keyring: a plaintext fake would miss authentication failures.
|
|
16
|
+
keyring;
|
|
17
|
+
constructor({ keyring }) { this.keyring = keyring; }
|
|
18
|
+
/** Encrypts under the requested generation with a fresh IV; AAD must be reconstructed on open. */
|
|
19
|
+
async seal(input) {
|
|
20
|
+
if (typeof input.aad !== "string")
|
|
21
|
+
throw new TypeError("AesGcmSecretSealer: aad must be a string");
|
|
22
|
+
const aesKey = await this.deriveAesKey(input.key);
|
|
23
|
+
const iv = randomBytes(IV_LENGTH_BYTES);
|
|
24
|
+
// GCM must never reuse an IV under the same key. AAD is authenticated but not stored in the
|
|
25
|
+
// envelope, so callers must reconstruct the byte-identical context when opening the record.
|
|
26
|
+
const cipher = createCipheriv(ALG, aesKey, iv);
|
|
27
|
+
cipher.setAAD(Buffer.from(input.aad, "utf8"));
|
|
28
|
+
const ciphertext = Buffer.concat([cipher.update(input.plaintext, "utf8"), cipher.final()]);
|
|
29
|
+
const authTag = cipher.getAuthTag();
|
|
30
|
+
return {
|
|
31
|
+
keyId: input.key.keyId,
|
|
32
|
+
ciphertext: Buffer.concat([ciphertext, authTag]).toString("base64"),
|
|
33
|
+
nonce: iv.toString("base64"),
|
|
34
|
+
alg: ALG,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Opens under the envelope's generation. Wrong key, tag, nonce, or AAD fails authentication. */
|
|
38
|
+
async open(input, { aad } = {}) {
|
|
39
|
+
if (input.sealed.alg !== ALG) {
|
|
40
|
+
throw new Error(`AesGcmSecretSealer cannot open alg '${input.sealed.alg}' (expected '${ALG}')`);
|
|
41
|
+
}
|
|
42
|
+
const aesKey = await this.deriveAesKey({ keyId: input.sealed.keyId });
|
|
43
|
+
// Use the stored generation, not the active one, so rotation does not strand older ciphertext.
|
|
44
|
+
// Derivation/authentication failures propagate; there is no plaintext or partial-output fallback.
|
|
45
|
+
const iv = Buffer.from(input.sealed.nonce, "base64");
|
|
46
|
+
const combined = Buffer.from(input.sealed.ciphertext, "base64");
|
|
47
|
+
if (combined.length < AUTH_TAG_LENGTH_BYTES) {
|
|
48
|
+
throw new Error("AesGcmSecretSealer: ciphertext too short to contain an auth tag");
|
|
49
|
+
}
|
|
50
|
+
const authTag = combined.subarray(combined.length - AUTH_TAG_LENGTH_BYTES);
|
|
51
|
+
const ciphertext = combined.subarray(0, combined.length - AUTH_TAG_LENGTH_BYTES);
|
|
52
|
+
const decipher = createDecipheriv(ALG, aesKey, iv);
|
|
53
|
+
if (aad !== undefined) {
|
|
54
|
+
decipher.setAAD(Buffer.from(aad, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
decipher.setAuthTag(authTag);
|
|
57
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
58
|
+
return plaintext.toString("utf8");
|
|
59
|
+
}
|
|
60
|
+
async deriveAesKey(key) {
|
|
61
|
+
return this.keyring.derive({ workspaceId: SEALER_KEY_SCOPE, purpose: SEALER_PURPOSE, info: key.keyId });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=secret-sealer.aesgcm.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Persisted envelope: base64 ciphertext followed by the GCM tag, plus a base64 IV. */
|
|
2
|
+
export interface SealedSecret {
|
|
3
|
+
keyId: string;
|
|
4
|
+
/** Base64 of encrypted bytes followed by the 16-byte authentication tag. */
|
|
5
|
+
ciphertext: string;
|
|
6
|
+
/** Base64 of the 12-byte IV. */
|
|
7
|
+
nonce: string;
|
|
8
|
+
alg: string;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/shell.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module shell
|
|
3
3
|
*
|
|
4
|
-
* Buffered command execution
|
|
4
|
+
* Buffered command execution through the user's selected shell. A daemon
|
|
5
5
|
* process's own `PATH` frequently lacks entries a shell profile adds (a
|
|
6
6
|
* version manager's shims, a package-manager-installed CLI, a Homebrew
|
|
7
7
|
* prefix), so a plain `execFile` against a bare command name can fail even
|
|
8
8
|
* though the same command works fine in the user's terminal. Routing through
|
|
9
|
-
* `$SHELL -c "..."`
|
|
9
|
+
* `$SHELL -c "..."` preserves the daemon's current PATH before running the
|
|
10
|
+
* command; it does not load login profiles or recover profile-only entries.
|
|
11
|
+
* The host must supply those entries in its environment when needed. A login
|
|
12
|
+
* invocation could reset PATH and hide wrapped command shims or test fakes.
|
|
10
13
|
*
|
|
11
14
|
* Depends on nothing else in this package; owns no process lifecycle beyond
|
|
12
15
|
* the single buffered child it spawns per call.
|
|
@@ -31,10 +34,11 @@ export type BufferedCommandResult = {
|
|
|
31
34
|
*/
|
|
32
35
|
export declare function execFileBuffered(command: string, args: readonly string[], opts?: ExecFileOptions): Promise<BufferedCommandResult>;
|
|
33
36
|
/**
|
|
34
|
-
* Run an arbitrary command
|
|
35
|
-
* `$SHELL -c`)
|
|
36
|
-
* its output.
|
|
37
|
-
*
|
|
37
|
+
* Run an arbitrary command through the user's selected shell (via
|
|
38
|
+
* `$SHELL -c`), explicitly preserving the current `process.env.PATH`, then
|
|
39
|
+
* buffer its output. The historical name is retained for compatibility;
|
|
40
|
+
* this non-login invocation does not load shell-profile-only PATH entries.
|
|
41
|
+
* Falls back to a direct `execFile` on Windows.
|
|
38
42
|
*
|
|
39
43
|
* @param command - The executable to run.
|
|
40
44
|
* @param args - Arguments to pass to the executable.
|