@jini-ai/platform 0.5.9 → 0.5.10
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 +7 -0
- package/dist/asset-cache.js +26 -25
- package/dist/blob-storage.d.ts +7 -2
- package/dist/blob-storage.js +99 -59
- package/dist/child-environment.d.ts +36 -0
- package/dist/child-environment.js +68 -0
- package/dist/download.js +192 -148
- package/dist/fs/atomic-write.js +105 -93
- package/dist/fs/boot-identity.d.ts +16 -0
- package/dist/fs/boot-identity.js +38 -17
- package/dist/fs/directory-home.js +6 -0
- package/dist/fs/directory-registry.d.ts +1 -1
- package/dist/fs/directory-registry.js +23 -16
- package/dist/fs/directory-running-lock.d.ts +1 -1
- package/dist/fs/directory-running-lock.js +18 -7
- package/dist/fs/directory-sync.d.ts +1 -1
- package/dist/fs/directory-sync.js +64 -51
- package/dist/fs/directory-user-config.js +108 -67
- package/dist/fs/directory-workspace.d.ts +11 -0
- package/dist/fs/directory-workspace.js +16 -11
- package/dist/fs/durable-json.js +49 -37
- package/dist/fs/file-lock.js +214 -149
- package/dist/fs/guarded-reader.js +47 -35
- package/dist/fs/user-data-directories.d.ts +1 -1
- package/dist/fs/user-data-directories.js +25 -9
- package/dist/fs/verified-directory-move.d.ts +13 -11
- package/dist/fs/verified-directory-move.js +105 -68
- package/dist/fs/verified-file-relocation.d.ts +1 -1
- package/dist/fs/verified-file-relocation.js +111 -94
- package/dist/http/guarded/client.d.ts +3 -4
- package/dist/http/guarded/client.js +50 -60
- package/dist/http/guarded/fetch-adapter.d.ts +2 -3
- package/dist/http/guarded/fetch-adapter.js +26 -15
- package/dist/mail/builtin-providers.d.ts +39 -0
- package/dist/mail/builtin-providers.js +60 -0
- package/dist/mail/feedback.d.ts +84 -0
- package/dist/mail/feedback.js +90 -0
- package/dist/mail/index.d.ts +11 -0
- package/dist/mail/index.js +6 -0
- package/dist/mail/policy.d.ts +77 -0
- package/dist/mail/policy.js +155 -0
- package/dist/mail/ports.d.ts +56 -7
- package/dist/mail/provider.d.ts +117 -0
- package/dist/mail/provider.js +74 -0
- package/dist/mail/rate-limit.d.ts +33 -0
- package/dist/mail/rate-limit.js +35 -0
- package/dist/mail/sender-identity.d.ts +68 -0
- package/dist/mail/sender-identity.js +50 -0
- package/dist/mail/types.d.ts +9 -0
- package/dist/net/address.d.ts +9 -23
- package/dist/net/address.js +28 -133
- package/dist/net/endpoint-policy.d.ts +26 -2
- package/dist/net/endpoint-policy.js +159 -56
- package/dist/object-store/index.d.ts +2 -0
- package/dist/object-store/index.js +2 -0
- package/dist/object-store/local-fs.d.ts +17 -0
- package/dist/object-store/local-fs.js +146 -0
- package/dist/process.js +2 -0
- package/dist/proxy-env.js +131 -135
- package/dist/secrets/credential-sets/contracts.d.ts +1 -0
- package/dist/secrets/credential-sets/contracts.js +1 -0
- package/dist/secrets/credential-sets/index.d.ts +1 -0
- package/dist/secrets/credential-sets/index.js +1 -0
- package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
- package/dist/secrets/credential-sets/provider-matches.js +61 -0
- package/dist/secrets/credential-sets/types.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.meta.js +58 -20
- package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
- package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
- package/dist/secrets/credential-sets/vault.service.js +131 -61
- package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
- package/dist/secrets/credential-sets/vault.types.js +22 -8
- package/dist/secrets/keyring.env.js +14 -10
- package/dist/toolchain.js +119 -91
- package/dist/web/html-document.js +73 -69
- package/dist/web/html-to-markdown.js +127 -102
- package/dist/web/web-page.js +160 -80
- package/package.json +11 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The default `local` object-store provider: keys are paths under one root directory.
|
|
3
|
+
*
|
|
4
|
+
* Path formula is `join(rootDir, key)` — byte-identical to `@jini-ai/cms`'s `LocalFsBlobStore`
|
|
5
|
+
* (`resolvePath`), so a host that moves its media from that class to this store reads every
|
|
6
|
+
* existing upload unchanged (no data migration). Unlike that class, keys here come from callers,
|
|
7
|
+
* so a key that would resolve outside `rootDir` (`..`, absolute) is refused before any syscall.
|
|
8
|
+
*
|
|
9
|
+
* Lexical containment alone is not enough: `<root>/link -> /elsewhere` makes `link/victim` look
|
|
10
|
+
* contained while every syscall follows the link out of the root. So each operation also checks
|
|
11
|
+
* the key's parent *physically* (realpath of its nearest existing ancestor against the realpath
|
|
12
|
+
* of the root), and the final component is opened with `O_NOFOLLOW` / `lstat`ed, so a symlink
|
|
13
|
+
* planted as the object itself is refused rather than followed.
|
|
14
|
+
*/
|
|
15
|
+
import { constants } from 'node:fs';
|
|
16
|
+
import { lstat, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
18
|
+
import { providerOk } from '@jini-ai/core/provider-registry';
|
|
19
|
+
/** Node's errno-carrying error, narrowed without a cast at each call site. */
|
|
20
|
+
function errnoCode(error) {
|
|
21
|
+
return error?.code;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolves `key` under `rootDir`, refusing any key that escapes it.
|
|
25
|
+
* @throws {Error} The key is empty, absolute, or resolves outside the root.
|
|
26
|
+
* @complexity O(key length).
|
|
27
|
+
*/
|
|
28
|
+
function contained(required) {
|
|
29
|
+
const { rootDir, key } = required;
|
|
30
|
+
const path = join(rootDir, key);
|
|
31
|
+
const rel = relative(resolve(rootDir), resolve(path));
|
|
32
|
+
if (key === '' || isAbsolute(key) || rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
33
|
+
throw new Error(`object key '${key}' is outside the store root`);
|
|
34
|
+
}
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
// `O_NOFOLLOW` makes the open itself fail (`ELOOP`) when the final component is a symlink, so a link
|
|
38
|
+
// swapped in after the containment check cannot redirect the read/write. Where the platform lacks it
|
|
39
|
+
// (Windows) the constant is undefined, which a bitwise OR coerces to 0 — degrading to the physical
|
|
40
|
+
// parent check alone.
|
|
41
|
+
const NOFOLLOW = constants.O_NOFOLLOW;
|
|
42
|
+
const READ_FLAGS = constants.O_RDONLY | NOFOLLOW;
|
|
43
|
+
const WRITE_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | NOFOLLOW;
|
|
44
|
+
const CREATE_ONLY_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW;
|
|
45
|
+
/**
|
|
46
|
+
* Realpath of `path`, or of its nearest existing ancestor with the missing tail re-appended.
|
|
47
|
+
* Recursion terminates because the filesystem root always exists; any fault other than absence
|
|
48
|
+
* (`ENOTDIR`, `EACCES`, `ELOOP`) rejects.
|
|
49
|
+
* @complexity O(depth) syscalls.
|
|
50
|
+
*/
|
|
51
|
+
async function realpathOfNearest(required) {
|
|
52
|
+
const { path } = required;
|
|
53
|
+
try {
|
|
54
|
+
return await realpath(path);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (errnoCode(error) !== 'ENOENT')
|
|
58
|
+
throw error;
|
|
59
|
+
return join(await realpathOfNearest({ path: dirname(path) }), basename(path));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolves `key` lexically, then refuses it unless its parent directory is physically inside the
|
|
64
|
+
* root (so a directory symlink pointing out of the root cannot be traversed).
|
|
65
|
+
* @throws {Error} The key escapes the root lexically or through a symlinked ancestor.
|
|
66
|
+
* @complexity O(depth) syscalls.
|
|
67
|
+
*/
|
|
68
|
+
async function physicallyContained(required) {
|
|
69
|
+
const path = contained(required);
|
|
70
|
+
const realRoot = await realpathOfNearest({ path: resolve(required.rootDir) });
|
|
71
|
+
const rel = relative(realRoot, await realpathOfNearest({ path: dirname(resolve(path)) }));
|
|
72
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
73
|
+
throw new Error(`object key '${required.key}' is outside the store root`);
|
|
74
|
+
}
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Creates a local-filesystem `ObjectStore` rooted at `rootDir`.
|
|
79
|
+
* @complexity Each call is O(1) syscalls beyond the read/write itself.
|
|
80
|
+
*/
|
|
81
|
+
export function createLocalFsObjectStore(required, _optional = {}) {
|
|
82
|
+
const { rootDir } = required;
|
|
83
|
+
const pathOf = (key) => physicallyContained({ rootDir, key });
|
|
84
|
+
return {
|
|
85
|
+
async put({ key, bytes }, options = {}) {
|
|
86
|
+
const path = await pathOf(key);
|
|
87
|
+
await mkdir(dirname(path), { recursive: true });
|
|
88
|
+
if (!options.ifAbsent) {
|
|
89
|
+
await writeFile(path, bytes, { flag: WRITE_FLAGS });
|
|
90
|
+
return { written: true };
|
|
91
|
+
}
|
|
92
|
+
// `O_CREAT | O_EXCL` (`"wx"`) makes create-only a kernel-enforced atomic op: of two racing
|
|
93
|
+
// opens of the same path exactly one succeeds and the other gets `EEXIST`, so there is no
|
|
94
|
+
// window between "check" and "write" for a concurrent writer to land in.
|
|
95
|
+
try {
|
|
96
|
+
await writeFile(path, bytes, { flag: CREATE_ONLY_FLAGS });
|
|
97
|
+
return { written: true };
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (errnoCode(error) === 'EEXIST')
|
|
101
|
+
return { written: false };
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
async get({ key }) {
|
|
106
|
+
try {
|
|
107
|
+
return new Uint8Array(await readFile(await pathOf(key), { flag: READ_FLAGS }));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (errnoCode(error) === 'ENOENT')
|
|
111
|
+
return null;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
// Only ENOENT is absence; EACCES/ENOTDIR/EIO reject, so a caller never mistakes "this stat
|
|
116
|
+
// failed" for "the object was deleted". `lstat` (not `stat`) so a symlink is reported as a
|
|
117
|
+
// refusal instead of the size of whatever it points at.
|
|
118
|
+
async head({ key }) {
|
|
119
|
+
try {
|
|
120
|
+
const info = await lstat(await pathOf(key));
|
|
121
|
+
if (info.isSymbolicLink())
|
|
122
|
+
throw new Error(`object key '${key}' is a symbolic link`);
|
|
123
|
+
return { size: info.size };
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (errnoCode(error) === 'ENOENT')
|
|
127
|
+
return null;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
// `rm` unlinks a final-component symlink itself and never its target; the physical parent check
|
|
132
|
+
// keeps it from deleting through a symlinked directory.
|
|
133
|
+
async delete({ key }) {
|
|
134
|
+
await rm(await pathOf(key), { force: true });
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The `local` provider bound to one root directory (the host owns where bytes live; settings and
|
|
140
|
+
* credentials are ignored — local storage needs neither).
|
|
141
|
+
* @complexity O(1).
|
|
142
|
+
*/
|
|
143
|
+
export function createLocalFsObjectStoreProvider(required, _optional = {}) {
|
|
144
|
+
return { create: () => providerOk({ value: createLocalFsObjectStore({ rootDir: required.rootDir }) }) };
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=local-fs.js.map
|
package/dist/process.js
CHANGED
|
@@ -229,6 +229,7 @@ function parsePsOutput(stdout) {
|
|
|
229
229
|
/** @internal Enumerate process snapshots on POSIX via `ps`. */
|
|
230
230
|
async function listPosixProcessSnapshots() {
|
|
231
231
|
const stdout = await new Promise((resolveList, rejectList) => {
|
|
232
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Fixed process-enumeration utility intentionally uses the trusted host PATH; command and argv are code-owned.
|
|
232
233
|
execFile("ps", ["-axo", "pid=,ppid=,command="], { encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }, (error, out) => {
|
|
233
234
|
if (error)
|
|
234
235
|
rejectList(error);
|
|
@@ -245,6 +246,7 @@ async function listWindowsProcessSnapshots() {
|
|
|
245
246
|
"Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, CommandLine | ConvertTo-Json -Compress",
|
|
246
247
|
].join("; ");
|
|
247
248
|
const stdout = await new Promise((resolveList, rejectList) => {
|
|
249
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Fixed process-enumeration utility intentionally uses the trusted host PATH; command and argv are code-owned.
|
|
248
250
|
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], { encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }, (error, out) => {
|
|
249
251
|
if (error)
|
|
250
252
|
rejectList(error);
|
package/dist/proxy-env.js
CHANGED
|
@@ -60,21 +60,7 @@ function setCanonicalProxyEnvValue(env, canonicalKey, value, platform) {
|
|
|
60
60
|
export function mergeProxyAwareEnv(platform, ...sources) {
|
|
61
61
|
const merged = {};
|
|
62
62
|
for (const source of sources) {
|
|
63
|
-
const proxyEntries =
|
|
64
|
-
for (const [key, value] of Object.entries(source)) {
|
|
65
|
-
if (value == null)
|
|
66
|
-
continue;
|
|
67
|
-
const canonicalKey = canonicalProxyEnvKey(key);
|
|
68
|
-
if (canonicalKey) {
|
|
69
|
-
const current = proxyEntries.get(canonicalKey);
|
|
70
|
-
const preferLowercase = key === key.toLowerCase();
|
|
71
|
-
if (!current || preferLowercase || !current.preferLowercase) {
|
|
72
|
-
proxyEntries.set(canonicalKey, { preferLowercase, value });
|
|
73
|
-
}
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
merged[key] = value;
|
|
77
|
-
}
|
|
63
|
+
const proxyEntries = collectSourceProxyEntries({ merged, source });
|
|
78
64
|
for (const [canonicalKey, entry] of proxyEntries) {
|
|
79
65
|
setCanonicalProxyEnvValue(merged, canonicalKey, entry.value, platform);
|
|
80
66
|
}
|
|
@@ -84,6 +70,26 @@ export function mergeProxyAwareEnv(platform, ...sources) {
|
|
|
84
70
|
}
|
|
85
71
|
return merged;
|
|
86
72
|
}
|
|
73
|
+
/** @internal Copy one source's non-proxy keys into `merged` and return its proxy values keyed by canonical name,
|
|
74
|
+
* preferring a lowercase spelling when a source carries several case-variants of the same variable. */
|
|
75
|
+
function collectSourceProxyEntries(input) {
|
|
76
|
+
const proxyEntries = new Map();
|
|
77
|
+
for (const [key, value] of Object.entries(input.source)) {
|
|
78
|
+
if (value == null)
|
|
79
|
+
continue;
|
|
80
|
+
const canonicalKey = canonicalProxyEnvKey(key);
|
|
81
|
+
if (!canonicalKey) {
|
|
82
|
+
input.merged[key] = value;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const current = proxyEntries.get(canonicalKey);
|
|
86
|
+
const preferLowercase = key === key.toLowerCase();
|
|
87
|
+
if (!current || preferLowercase || !current.preferLowercase) {
|
|
88
|
+
proxyEntries.set(canonicalKey, { preferLowercase, value });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return proxyEntries;
|
|
92
|
+
}
|
|
87
93
|
/** @internal Whether `env` already contains any case-variant of the given canonical proxy key. */
|
|
88
94
|
function hasCanonicalProxyEnv(env, canonicalKey) {
|
|
89
95
|
return Object.keys(env).some((key) => key.toLowerCase() === canonicalKey.toLowerCase());
|
|
@@ -179,29 +185,28 @@ function normalizeHostPortProxyUrl(host, port, scheme) {
|
|
|
179
185
|
: trimmedHost;
|
|
180
186
|
return normalizeProxyUrl(`${normalizedHost}:${trimmedPort}`, scheme);
|
|
181
187
|
}
|
|
188
|
+
/** @internal The NO_PROXY value for a system proxy: a preserved wildcard, else the bypass list plus loopback hosts. */
|
|
189
|
+
function systemNoProxyValue(noProxy) {
|
|
190
|
+
return (preserveWildcardNoProxyValue(noProxy) ??
|
|
191
|
+
buildNoProxyValue([...(noProxy ? noProxy.split(",") : []), "localhost", "127.0.0.1", "[::1]"]));
|
|
192
|
+
}
|
|
182
193
|
/** @internal Assemble the final proxy env from parsed values, adding loopback bypasses and the `NODE_USE_ENV_PROXY` flag. */
|
|
183
194
|
function finalizeSystemProxyEnv(values, platform) {
|
|
184
|
-
const hasProxy = Boolean(values.httpProxy || values.httpsProxy || values.allProxy);
|
|
185
|
-
const noProxy = hasProxy
|
|
186
|
-
? preserveWildcardNoProxyValue(values.noProxy) ??
|
|
187
|
-
buildNoProxyValue([
|
|
188
|
-
...(values.noProxy ? values.noProxy.split(",") : []),
|
|
189
|
-
"localhost",
|
|
190
|
-
"127.0.0.1",
|
|
191
|
-
"[::1]",
|
|
192
|
-
])
|
|
193
|
-
: null;
|
|
194
195
|
const env = {};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
196
|
+
const hasProxy = Boolean(values.httpProxy || values.httpsProxy || values.allProxy);
|
|
197
|
+
if (!hasProxy)
|
|
198
|
+
return env;
|
|
199
|
+
const entries = [
|
|
200
|
+
["HTTP_PROXY", values.httpProxy],
|
|
201
|
+
["HTTPS_PROXY", values.httpsProxy],
|
|
202
|
+
["ALL_PROXY", values.allProxy],
|
|
203
|
+
["NO_PROXY", systemNoProxyValue(values.noProxy)],
|
|
204
|
+
];
|
|
205
|
+
for (const [key, value] of entries) {
|
|
206
|
+
if (value)
|
|
207
|
+
addProxyEnvValue(env, key, value, platform);
|
|
208
|
+
}
|
|
209
|
+
env.NODE_USE_ENV_PROXY = "1";
|
|
205
210
|
return env;
|
|
206
211
|
}
|
|
207
212
|
/**
|
|
@@ -214,55 +219,84 @@ function finalizeSystemProxyEnv(values, platform) {
|
|
|
214
219
|
* @returns A normalized proxy environment (empty when no proxy is enabled).
|
|
215
220
|
*/
|
|
216
221
|
export function parseMacosScutilProxyOutput(stdout, platform = "darwin") {
|
|
217
|
-
const scalars =
|
|
218
|
-
const exceptions = [];
|
|
219
|
-
let inExceptions = false;
|
|
220
|
-
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
221
|
-
const line = rawLine.trim();
|
|
222
|
-
if (!line)
|
|
223
|
-
continue;
|
|
224
|
-
if (/^ExceptionsList\s*:\s*<array>\s*\{$/.test(line)) {
|
|
225
|
-
inExceptions = true;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
if (inExceptions) {
|
|
229
|
-
if (line === "}") {
|
|
230
|
-
inExceptions = false;
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const match = line.match(/^\d+\s*:\s*(.+)$/);
|
|
234
|
-
if (match)
|
|
235
|
-
exceptions.push(match[1].trim());
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
const match = line.match(/^([A-Za-z][A-Za-z0-9]*)\s*:\s*(.+)$/);
|
|
239
|
-
if (match)
|
|
240
|
-
scalars.set(match[1], match[2].trim());
|
|
241
|
-
}
|
|
242
|
-
const httpProxy = scalars.get("HTTPEnable") === "1"
|
|
243
|
-
? normalizeHostPortProxyUrl(scalars.get("HTTPProxy"), scalars.get("HTTPPort"), "http")
|
|
244
|
-
: null;
|
|
245
|
-
const httpsProxy = scalars.get("HTTPSEnable") === "1"
|
|
246
|
-
? normalizeHostPortProxyUrl(scalars.get("HTTPSProxy"), scalars.get("HTTPSPort"), "http")
|
|
247
|
-
: null;
|
|
248
|
-
const allProxy = scalars.get("SOCKSEnable") === "1"
|
|
249
|
-
? normalizeHostPortProxyUrl(scalars.get("SOCKSProxy"), scalars.get("SOCKSPort"), "socks5")
|
|
250
|
-
: null;
|
|
222
|
+
const { exceptions, scalars } = parseScutilLines(stdout);
|
|
251
223
|
return finalizeSystemProxyEnv({
|
|
252
|
-
allProxy,
|
|
253
|
-
httpProxy,
|
|
254
|
-
httpsProxy,
|
|
224
|
+
allProxy: scutilEnabledProxy({ prefix: "SOCKS", scalars, scheme: "socks5" }),
|
|
225
|
+
httpProxy: scutilEnabledProxy({ prefix: "HTTP", scalars, scheme: "http" }),
|
|
226
|
+
httpsProxy: scutilEnabledProxy({ prefix: "HTTPS", scalars, scheme: "http" }),
|
|
255
227
|
noProxy: buildNoProxyValue([
|
|
256
228
|
...exceptions,
|
|
257
229
|
...(scalars.get("ExcludeSimpleHostnames") === "1" ? ["<local>"] : []),
|
|
258
230
|
]),
|
|
259
231
|
}, platform);
|
|
260
232
|
}
|
|
233
|
+
/** @internal Split `scutil --proxy` output into its `Name : value` scalars and its `ExceptionsList` array entries. */
|
|
234
|
+
function parseScutilLines(stdout) {
|
|
235
|
+
const scalars = new Map();
|
|
236
|
+
const exceptions = [];
|
|
237
|
+
let inExceptions = false;
|
|
238
|
+
// Blank lines need no explicit skip: they match neither the array-entry nor
|
|
239
|
+
// the scalar pattern, and are not the closing `}`.
|
|
240
|
+
for (const line of stdout.split(/\r?\n/).map((rawLine) => rawLine.trim())) {
|
|
241
|
+
if (/^ExceptionsList\s*:\s*<array>\s*\{$/.test(line))
|
|
242
|
+
inExceptions = true;
|
|
243
|
+
else if (inExceptions)
|
|
244
|
+
inExceptions = collectScutilException(line, exceptions);
|
|
245
|
+
else
|
|
246
|
+
collectScutilScalar(line, scalars);
|
|
247
|
+
}
|
|
248
|
+
return { exceptions, scalars };
|
|
249
|
+
}
|
|
250
|
+
/** @internal Record one `N : value` ExceptionsList entry; returns whether the array is still open (false on `}`). */
|
|
251
|
+
function collectScutilException(line, exceptions) {
|
|
252
|
+
const match = line.match(/^\d+\s*:\s*(.+)$/);
|
|
253
|
+
if (match)
|
|
254
|
+
exceptions.push(match[1].trim());
|
|
255
|
+
return line !== "}";
|
|
256
|
+
}
|
|
257
|
+
/** @internal Record one top-level `Name : value` scutil scalar, ignoring lines of any other shape. */
|
|
258
|
+
function collectScutilScalar(line, scalars) {
|
|
259
|
+
const match = line.match(/^([A-Za-z][A-Za-z0-9]*)\s*:\s*(.+)$/);
|
|
260
|
+
if (match)
|
|
261
|
+
scalars.set(match[1], match[2].trim());
|
|
262
|
+
}
|
|
263
|
+
/** @internal The `<prefix>Proxy`/`<prefix>Port` URL when `<prefix>Enable` is `1`, else `null`. */
|
|
264
|
+
function scutilEnabledProxy(input) {
|
|
265
|
+
const { prefix, scalars, scheme } = input;
|
|
266
|
+
if (scalars.get(`${prefix}Enable`) !== "1")
|
|
267
|
+
return null;
|
|
268
|
+
return normalizeHostPortProxyUrl(scalars.get(`${prefix}Proxy`), scalars.get(`${prefix}Port`), scheme);
|
|
269
|
+
}
|
|
261
270
|
/** @internal Extract a `reg query` value line (`Name REG_TYPE value`) for the named value, or `null` if absent. */
|
|
262
271
|
function parseRegistryValue(stdout, valueName) {
|
|
263
272
|
const match = stdout.match(new RegExp(`^\\s*${valueName}\\s+REG_\\w+\\s+(.+)$`, "m"));
|
|
264
273
|
return match ? match[1].trim() : null;
|
|
265
274
|
}
|
|
275
|
+
/** @internal Per-protocol `ProxyServer` kinds mapped to the env slot and default scheme they fill. */
|
|
276
|
+
const WINDOWS_PROXY_KINDS = new Map([
|
|
277
|
+
["http", { scheme: "http", slot: "httpProxy" }],
|
|
278
|
+
["https", { scheme: "http", slot: "httpsProxy" }],
|
|
279
|
+
["socks", { scheme: "socks5", slot: "allProxy" }],
|
|
280
|
+
]);
|
|
281
|
+
/** @internal Parse the per-protocol `http=…;https=…;socks=…` `ProxyServer` form; later segments win, unknown kinds are skipped. */
|
|
282
|
+
function parsePerProtocolProxyServer(proxyServer) {
|
|
283
|
+
const proxies = {};
|
|
284
|
+
for (const segment of proxyServer.split(";")) {
|
|
285
|
+
const [kind, rawValue] = segment.split("=", 2);
|
|
286
|
+
const value = rawValue?.trim();
|
|
287
|
+
if (!kind || !value)
|
|
288
|
+
continue;
|
|
289
|
+
const target = WINDOWS_PROXY_KINDS.get(kind.trim().toLowerCase());
|
|
290
|
+
if (target)
|
|
291
|
+
proxies[target.slot] = normalizeAuthorityProxyUrl(value, target.scheme);
|
|
292
|
+
}
|
|
293
|
+
return proxies;
|
|
294
|
+
}
|
|
295
|
+
/** @internal The single-endpoint `ProxyServer` form, shared by HTTP and HTTPS. */
|
|
296
|
+
function sharedProxyServer(proxyServer) {
|
|
297
|
+
const shared = normalizeAuthorityProxyUrl(proxyServer, "http");
|
|
298
|
+
return { httpProxy: shared, httpsProxy: shared };
|
|
299
|
+
}
|
|
266
300
|
/**
|
|
267
301
|
* Parse Windows Internet Settings proxy registry output into a normalized proxy
|
|
268
302
|
* environment. Handles both the per-protocol (`http=…;https=…`) and shared
|
|
@@ -280,36 +314,16 @@ export function parseWindowsInternetSettingsProxyOutput(input, platform = "win32
|
|
|
280
314
|
const proxyOverride = parseRegistryValue(input.proxyOverride ?? "", "ProxyOverride") ?? "";
|
|
281
315
|
if (!proxyServer.trim())
|
|
282
316
|
return {};
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (proxyServer.includes("=")) {
|
|
287
|
-
for (const segment of proxyServer.split(";")) {
|
|
288
|
-
const [kind, rawValue] = segment.split("=", 2);
|
|
289
|
-
const value = rawValue?.trim();
|
|
290
|
-
if (!kind || !value)
|
|
291
|
-
continue;
|
|
292
|
-
const lowerKind = kind.trim().toLowerCase();
|
|
293
|
-
if (lowerKind === "http")
|
|
294
|
-
httpProxy = normalizeAuthorityProxyUrl(value, "http");
|
|
295
|
-
else if (lowerKind === "https")
|
|
296
|
-
httpsProxy = normalizeAuthorityProxyUrl(value, "http");
|
|
297
|
-
else if (lowerKind === "socks")
|
|
298
|
-
allProxy = normalizeAuthorityProxyUrl(value, "socks5");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
const shared = normalizeAuthorityProxyUrl(proxyServer, "http");
|
|
303
|
-
httpProxy = shared;
|
|
304
|
-
httpsProxy = shared;
|
|
305
|
-
}
|
|
317
|
+
const proxies = proxyServer.includes("=")
|
|
318
|
+
? parsePerProtocolProxyServer(proxyServer)
|
|
319
|
+
: sharedProxyServer(proxyServer);
|
|
306
320
|
return finalizeSystemProxyEnv({
|
|
307
|
-
|
|
308
|
-
httpProxy,
|
|
309
|
-
httpsProxy,
|
|
321
|
+
...proxies,
|
|
310
322
|
noProxy: buildNoProxyValue(proxyOverride.split(/[;,]/)),
|
|
311
323
|
}, platform);
|
|
312
324
|
}
|
|
325
|
+
/** @internal Registry key holding the current user's Internet Settings proxy values. */
|
|
326
|
+
const WINDOWS_INTERNET_SETTINGS_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings";
|
|
313
327
|
/**
|
|
314
328
|
* Resolve the OS-level proxy configuration into a normalized proxy environment.
|
|
315
329
|
* Shells out to `scutil` on macOS and `reg query` on Windows (swallowing per-
|
|
@@ -321,6 +335,16 @@ export function parseWindowsInternetSettingsProxyOutput(input, platform = "win32
|
|
|
321
335
|
export function resolveSystemProxyEnv(options = {}) {
|
|
322
336
|
const platform = options.platform ?? process.platform;
|
|
323
337
|
const runCommand = options.runCommand ?? defaultSystemProxyCommandRunner;
|
|
338
|
+
// No outer try/catch around the parse calls below (a former defense-in-depth
|
|
339
|
+
// catch was removed as unreachable): `tryRun` already swallows every
|
|
340
|
+
// `runCommand` failure, and neither parse
|
|
341
|
+
// function (`parseMacosScutilProxyOutput` / the Windows one) nor
|
|
342
|
+
// anything they call performs any operation that can throw for a
|
|
343
|
+
// string input — no `new URL`, no `JSON.parse`, no fixed-pattern
|
|
344
|
+
// `RegExp` construction from untrusted input. Empirically re-verified
|
|
345
|
+
// this session: 200k fuzzed `stdout`/registry-value strings (control
|
|
346
|
+
// characters, huge lengths, null bytes) through both parse functions,
|
|
347
|
+
// zero throws — see archived provenance ledger's 2026-07-22 entry.
|
|
324
348
|
const tryRun = (command, args) => {
|
|
325
349
|
try {
|
|
326
350
|
return runCommand(command, args);
|
|
@@ -329,44 +353,16 @@ export function resolveSystemProxyEnv(options = {}) {
|
|
|
329
353
|
return "";
|
|
330
354
|
}
|
|
331
355
|
};
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
if (platform === "win32") {
|
|
337
|
-
return parseWindowsInternetSettingsProxyOutput({
|
|
338
|
-
proxyEnable: tryRun("reg", [
|
|
339
|
-
"query",
|
|
340
|
-
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
|
341
|
-
"/v",
|
|
342
|
-
"ProxyEnable",
|
|
343
|
-
]),
|
|
344
|
-
proxyOverride: tryRun("reg", [
|
|
345
|
-
"query",
|
|
346
|
-
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
|
347
|
-
"/v",
|
|
348
|
-
"ProxyOverride",
|
|
349
|
-
]),
|
|
350
|
-
proxyServer: tryRun("reg", [
|
|
351
|
-
"query",
|
|
352
|
-
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
|
353
|
-
"/v",
|
|
354
|
-
"ProxyServer",
|
|
355
|
-
]),
|
|
356
|
-
}, platform);
|
|
357
|
-
}
|
|
356
|
+
const regQuery = (valueName) => tryRun("reg", ["query", WINDOWS_INTERNET_SETTINGS_KEY, "/v", valueName]);
|
|
357
|
+
if (platform === "darwin") {
|
|
358
|
+
return parseMacosScutilProxyOutput(tryRun("scutil", ["--proxy"]), platform);
|
|
358
359
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// `RegExp` construction from untrusted input. Empirically re-verified
|
|
366
|
-
// this session: 200k fuzzed `stdout`/registry-value strings (control
|
|
367
|
-
// characters, huge lengths, null bytes) through both parse functions,
|
|
368
|
-
// zero throws — see archived provenance ledger's 2026-07-22 entry.
|
|
369
|
-
return {};
|
|
360
|
+
if (platform === "win32") {
|
|
361
|
+
return parseWindowsInternetSettingsProxyOutput({
|
|
362
|
+
proxyEnable: regQuery("ProxyEnable"),
|
|
363
|
+
proxyOverride: regQuery("ProxyOverride"),
|
|
364
|
+
proxyServer: regQuery("ProxyServer"),
|
|
365
|
+
}, platform);
|
|
370
366
|
}
|
|
371
367
|
return {};
|
|
372
368
|
}
|
|
@@ -4,4 +4,5 @@ export { buildVaultAad } from "./vault.aad.js";
|
|
|
4
4
|
export { createVaultMetaParsers, isCopyableKind } from "./vault.meta.js";
|
|
5
5
|
export { SecretsVaultService, type VaultServiceDeps, type VaultPutInput, type VaultCopyInput } from "./vault.service.js";
|
|
6
6
|
export { InMemoryVaultRepo } from "./vault.repo.memory.js";
|
|
7
|
+
export { matchingCredentialProviderIds, selectCredentialForProvider, suggestCredentialWorksFor, normalizeCredentialWorksFor, type CredentialProviderDescriptor, type MatchableCredential } from "./provider-matches.js";
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,4 +3,5 @@ export { buildVaultAad } from "./vault.aad.js";
|
|
|
3
3
|
export { createVaultMetaParsers, isCopyableKind } from "./vault.meta.js";
|
|
4
4
|
export { SecretsVaultService } from "./vault.service.js";
|
|
5
5
|
export { InMemoryVaultRepo } from "./vault.repo.memory.js";
|
|
6
|
+
export { matchingCredentialProviderIds, selectCredentialForProvider, suggestCredentialWorksFor, normalizeCredentialWorksFor } from "./provider-matches.js";
|
|
6
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type VaultOptions } from "./vault.types.js";
|
|
2
|
+
/** Host-supplied catalog identity and accepted payload shape; catalogs never live in the vault. */
|
|
3
|
+
export interface CredentialProviderDescriptor {
|
|
4
|
+
id: string;
|
|
5
|
+
vendorId: string;
|
|
6
|
+
acceptsCustomToken: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Safe metadata needed for matching. Labels and provider groups never imply compatibility. */
|
|
9
|
+
export interface MatchableCredential {
|
|
10
|
+
kind: string;
|
|
11
|
+
provider: string | null;
|
|
12
|
+
meta: {
|
|
13
|
+
credentialType?: unknown;
|
|
14
|
+
worksFor?: unknown;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Validate and canonicalize explicit compatibility IDs without consulting a catalog.
|
|
18
|
+
* @throws VaultError for non-array, non-string, empty or whitespace-bearing IDs.
|
|
19
|
+
* @complexity O(total ID characters) time and space; does not mutate the supplied array.
|
|
20
|
+
* @example normalizeCredentialWorksFor({ value: ["GitHub", "github"] }); // ["github"]
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalizeCredentialWorksFor({ value }: {
|
|
23
|
+
value: unknown;
|
|
24
|
+
}, _options?: VaultOptions): string[];
|
|
25
|
+
/** Return compatible host IDs from saved metadata, respecting the consumer's payload shape.
|
|
26
|
+
* Historical custom-kind rows use the same parsed metadata until migration. Unknown kinds and
|
|
27
|
+
* malformed metadata are ineligible. Performs no inference or I/O.
|
|
28
|
+
* @complexity O(worksFor + providers) time, O(worksFor + matches) space.
|
|
29
|
+
* @example matchingCredentialProviderIds({ credential, providers });
|
|
30
|
+
*/
|
|
31
|
+
export declare function matchingCredentialProviderIds({ credential, providers }: {
|
|
32
|
+
credential: MatchableCredential;
|
|
33
|
+
providers: readonly CredentialProviderDescriptor[];
|
|
34
|
+
}, _options?: VaultOptions): string[];
|
|
35
|
+
/** Select an eligible explicit ID, the descriptor's provider-group default, or a sole candidate.
|
|
36
|
+
* Missing IDs and unresolved ambiguity return null; custom-group defaults do not rank candidates.
|
|
37
|
+
* @complexity O(credentials * average worksFor) time, O(credentials) candidate space.
|
|
38
|
+
* @example selectCredentialForProvider({ credentials, provider, id: selectedId });
|
|
39
|
+
*/
|
|
40
|
+
export declare function selectCredentialForProvider<T extends MatchableCredential & {
|
|
41
|
+
id: string;
|
|
42
|
+
isDefault: boolean;
|
|
43
|
+
}>({ credentials, provider, id, }: {
|
|
44
|
+
credentials: readonly T[];
|
|
45
|
+
provider: CredentialProviderDescriptor;
|
|
46
|
+
id?: string;
|
|
47
|
+
}, _options?: VaultOptions): T | null;
|
|
48
|
+
/** Suggest one explicit ID for create-time prefill only, using an injected catalog.
|
|
49
|
+
* Matches exact spelling ignoring case; whitespace/substrings are never normalized. A spelling
|
|
50
|
+
* shared by conflicting vendor groups is ambiguous, while repeated hosts in one group are safe.
|
|
51
|
+
* @complexity O(providers) time, O(matching vendor groups) space; no mutation or I/O.
|
|
52
|
+
* @example suggestCredentialWorksFor({ label: "GitHub", providers }); // ["github"]
|
|
53
|
+
*/
|
|
54
|
+
export declare function suggestCredentialWorksFor({ label, providers }: {
|
|
55
|
+
label: string;
|
|
56
|
+
providers: readonly {
|
|
57
|
+
id: string;
|
|
58
|
+
vendorId: string;
|
|
59
|
+
}[];
|
|
60
|
+
}, _options?: VaultOptions): string[];
|
|
61
|
+
//# sourceMappingURL=provider-matches.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { VaultError } from "./vault.types.js";
|
|
2
|
+
/** Validate and canonicalize explicit compatibility IDs without consulting a catalog.
|
|
3
|
+
* @throws VaultError for non-array, non-string, empty or whitespace-bearing IDs.
|
|
4
|
+
* @complexity O(total ID characters) time and space; does not mutate the supplied array.
|
|
5
|
+
* @example normalizeCredentialWorksFor({ value: ["GitHub", "github"] }); // ["github"]
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeCredentialWorksFor({ value }, _options = {}) {
|
|
8
|
+
if (!Array.isArray(value))
|
|
9
|
+
throw new VaultError({ code: "invalid-meta" });
|
|
10
|
+
const ids = new Set();
|
|
11
|
+
for (const id of value) {
|
|
12
|
+
if (typeof id !== "string" || !id || /\s/.test(id))
|
|
13
|
+
throw new VaultError({ code: "invalid-meta" });
|
|
14
|
+
ids.add(id.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
return [...ids];
|
|
17
|
+
}
|
|
18
|
+
/** Return compatible host IDs from saved metadata, respecting the consumer's payload shape.
|
|
19
|
+
* Historical custom-kind rows use the same parsed metadata until migration. Unknown kinds and
|
|
20
|
+
* malformed metadata are ineligible. Performs no inference or I/O.
|
|
21
|
+
* @complexity O(worksFor + providers) time, O(worksFor + matches) space.
|
|
22
|
+
* @example matchingCredentialProviderIds({ credential, providers });
|
|
23
|
+
*/
|
|
24
|
+
export function matchingCredentialProviderIds({ credential, providers }, _options = {}) {
|
|
25
|
+
const { credentialType, worksFor } = credential.meta;
|
|
26
|
+
if ((credential.kind !== "vendor" && credential.kind !== "custom") ||
|
|
27
|
+
(credential.kind === "custom" && credentialType !== "custom") || (credentialType !== "vendor" && credentialType !== "custom") ||
|
|
28
|
+
!Array.isArray(worksFor) || Array.from(worksFor).some(id => typeof id !== "string" || !id || id !== id.toLowerCase() || /\s/.test(id)))
|
|
29
|
+
return [];
|
|
30
|
+
const ids = new Set(worksFor);
|
|
31
|
+
return providers.filter(provider => (credentialType !== "custom" || provider.acceptsCustomToken) &&
|
|
32
|
+
(ids.has(provider.id.toLowerCase()) || ids.has(provider.vendorId.toLowerCase())))
|
|
33
|
+
.map(provider => provider.id);
|
|
34
|
+
}
|
|
35
|
+
/** Select an eligible explicit ID, the descriptor's provider-group default, or a sole candidate.
|
|
36
|
+
* Missing IDs and unresolved ambiguity return null; custom-group defaults do not rank candidates.
|
|
37
|
+
* @complexity O(credentials * average worksFor) time, O(credentials) candidate space.
|
|
38
|
+
* @example selectCredentialForProvider({ credentials, provider, id: selectedId });
|
|
39
|
+
*/
|
|
40
|
+
export function selectCredentialForProvider({ credentials, provider, id, }, _options = {}) {
|
|
41
|
+
const candidates = credentials.filter(credential => matchingCredentialProviderIds({ credential, providers: [provider] }).length > 0);
|
|
42
|
+
if (id !== undefined)
|
|
43
|
+
return candidates.find(credential => credential.id === id) ?? null;
|
|
44
|
+
const defaults = candidates.filter(credential => credential.provider === provider.vendorId && credential.isDefault);
|
|
45
|
+
if (defaults.length === 1)
|
|
46
|
+
return defaults[0];
|
|
47
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
48
|
+
}
|
|
49
|
+
/** Suggest one explicit ID for create-time prefill only, using an injected catalog.
|
|
50
|
+
* Matches exact spelling ignoring case; whitespace/substrings are never normalized. A spelling
|
|
51
|
+
* shared by conflicting vendor groups is ambiguous, while repeated hosts in one group are safe.
|
|
52
|
+
* @complexity O(providers) time, O(matching vendor groups) space; no mutation or I/O.
|
|
53
|
+
* @example suggestCredentialWorksFor({ label: "GitHub", providers }); // ["github"]
|
|
54
|
+
*/
|
|
55
|
+
export function suggestCredentialWorksFor({ label, providers }, _options = {}) {
|
|
56
|
+
const identity = label.toLowerCase();
|
|
57
|
+
const vendors = new Set(providers.filter(provider => provider.id.toLowerCase() === identity ||
|
|
58
|
+
provider.vendorId.toLowerCase() === identity).map(provider => provider.vendorId.toLowerCase()));
|
|
59
|
+
return identity && vendors.size === 1 ? [identity] : [];
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=provider-matches.js.map
|