@jini-ai/platform 0.5.8 → 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.
Files changed (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/asset-cache.js +26 -25
  3. package/dist/blob-storage.d.ts +7 -2
  4. package/dist/blob-storage.js +99 -59
  5. package/dist/child-environment.d.ts +36 -0
  6. package/dist/child-environment.js +68 -0
  7. package/dist/download.js +192 -148
  8. package/dist/fs/atomic-write.d.ts +25 -2
  9. package/dist/fs/atomic-write.js +162 -111
  10. package/dist/fs/boot-identity.d.ts +16 -0
  11. package/dist/fs/boot-identity.js +38 -17
  12. package/dist/fs/directory-home.js +6 -0
  13. package/dist/fs/directory-registry.d.ts +1 -1
  14. package/dist/fs/directory-registry.js +23 -16
  15. package/dist/fs/directory-running-lock.d.ts +1 -1
  16. package/dist/fs/directory-running-lock.js +18 -7
  17. package/dist/fs/directory-sync.d.ts +1 -1
  18. package/dist/fs/directory-sync.js +64 -51
  19. package/dist/fs/directory-user-config.js +108 -67
  20. package/dist/fs/directory-workspace.d.ts +11 -0
  21. package/dist/fs/directory-workspace.js +16 -11
  22. package/dist/fs/durable-json.d.ts +3 -1
  23. package/dist/fs/durable-json.js +55 -40
  24. package/dist/fs/file-lock.js +214 -149
  25. package/dist/fs/guarded-reader.js +47 -35
  26. package/dist/fs/user-data-directories.d.ts +1 -1
  27. package/dist/fs/user-data-directories.js +25 -9
  28. package/dist/fs/verified-directory-move.d.ts +14 -11
  29. package/dist/fs/verified-directory-move.js +106 -80
  30. package/dist/fs/verified-file-relocation.d.ts +2 -1
  31. package/dist/fs/verified-file-relocation.js +112 -100
  32. package/dist/http/guarded/client.d.ts +3 -4
  33. package/dist/http/guarded/client.js +50 -60
  34. package/dist/http/guarded/fetch-adapter.d.ts +2 -3
  35. package/dist/http/guarded/fetch-adapter.js +26 -15
  36. package/dist/mail/builtin-providers.d.ts +39 -0
  37. package/dist/mail/builtin-providers.js +60 -0
  38. package/dist/mail/feedback.d.ts +84 -0
  39. package/dist/mail/feedback.js +90 -0
  40. package/dist/mail/index.d.ts +11 -0
  41. package/dist/mail/index.js +6 -0
  42. package/dist/mail/policy.d.ts +77 -0
  43. package/dist/mail/policy.js +155 -0
  44. package/dist/mail/ports.d.ts +56 -7
  45. package/dist/mail/provider.d.ts +117 -0
  46. package/dist/mail/provider.js +74 -0
  47. package/dist/mail/rate-limit.d.ts +33 -0
  48. package/dist/mail/rate-limit.js +35 -0
  49. package/dist/mail/sender-identity.d.ts +68 -0
  50. package/dist/mail/sender-identity.js +50 -0
  51. package/dist/mail/types.d.ts +9 -0
  52. package/dist/net/address.d.ts +9 -23
  53. package/dist/net/address.js +28 -133
  54. package/dist/net/endpoint-policy.d.ts +26 -2
  55. package/dist/net/endpoint-policy.js +159 -56
  56. package/dist/object-store/index.d.ts +2 -0
  57. package/dist/object-store/index.js +2 -0
  58. package/dist/object-store/local-fs.d.ts +17 -0
  59. package/dist/object-store/local-fs.js +146 -0
  60. package/dist/process.js +2 -0
  61. package/dist/proxy-env.js +131 -135
  62. package/dist/secrets/credential-sets/contracts.d.ts +1 -0
  63. package/dist/secrets/credential-sets/contracts.js +1 -0
  64. package/dist/secrets/credential-sets/index.d.ts +1 -0
  65. package/dist/secrets/credential-sets/index.js +1 -0
  66. package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
  67. package/dist/secrets/credential-sets/provider-matches.js +61 -0
  68. package/dist/secrets/credential-sets/types.d.ts +7 -0
  69. package/dist/secrets/credential-sets/vault.meta.js +58 -20
  70. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
  71. package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
  72. package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
  73. package/dist/secrets/credential-sets/vault.service.js +131 -61
  74. package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
  75. package/dist/secrets/credential-sets/vault.types.js +22 -8
  76. package/dist/secrets/keyring.env.js +14 -10
  77. package/dist/toolchain.js +119 -91
  78. package/dist/web/html-document.js +73 -69
  79. package/dist/web/html-to-markdown.js +127 -102
  80. package/dist/web/web-page.d.ts +1 -1
  81. package/dist/web/web-page.js +162 -81
  82. 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 = new Map();
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
- if (values.httpProxy)
196
- addProxyEnvValue(env, "HTTP_PROXY", values.httpProxy, platform);
197
- if (values.httpsProxy)
198
- addProxyEnvValue(env, "HTTPS_PROXY", values.httpsProxy, platform);
199
- if (values.allProxy)
200
- addProxyEnvValue(env, "ALL_PROXY", values.allProxy, platform);
201
- if (noProxy)
202
- addProxyEnvValue(env, "NO_PROXY", noProxy, platform);
203
- if (hasProxy)
204
- env.NODE_USE_ENV_PROXY = "1";
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 = new Map();
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
- let httpProxy = null;
284
- let httpsProxy = null;
285
- let allProxy = null;
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
- allProxy,
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
- try {
333
- if (platform === "darwin") {
334
- return parseMacosScutilProxyOutput(tryRun("scutil", ["--proxy"]), platform);
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
- catch {
360
- // Defense-in-depth, not reachable through any real call path: `tryRun`
361
- // already swallows every `runCommand` failure, and neither parse
362
- // function (`parseMacosScutilProxyOutput` / the Windows one) nor
363
- // anything they call performs any operation that can throw for a
364
- // string input — no `new URL`, no `JSON.parse`, no fixed-pattern
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
  }
@@ -3,4 +3,5 @@
3
3
  * @example import { VaultError, type VaultRepoPort } from "@jini-ai/platform/secrets/credential-sets/contracts";
4
4
  */
5
5
  export * from "./vault.types.js";
6
+ export { suggestCredentialWorksFor } from "./provider-matches.js";
6
7
  //# sourceMappingURL=contracts.d.ts.map
@@ -3,4 +3,5 @@
3
3
  * @example import { VaultError, type VaultRepoPort } from "@jini-ai/platform/secrets/credential-sets/contracts";
4
4
  */
5
5
  export * from "./vault.types.js";
6
+ export { suggestCredentialWorksFor } from "./provider-matches.js";
6
7
  //# sourceMappingURL=contracts.js.map
@@ -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