@jini-ai/platform 0.6.15-rc.2 → 0.6.16
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 +26 -0
- package/README.md +7 -8
- package/dist/asset-cache.adapter.d.ts +2 -2
- package/dist/asset-cache.adapter.js +2 -2
- package/dist/asset-cache.d.ts +1 -3
- package/dist/asset-cache.js +2 -14
- package/dist/blob-storage.js +10 -9
- package/dist/child-environment.js +1 -1
- package/dist/command.js +4 -1
- package/dist/core/effective-port.d.ts +6 -0
- package/dist/core/effective-port.js +8 -0
- package/dist/download.js +25 -24
- package/dist/fetch-with-timeout.d.ts +27 -7
- package/dist/fetch-with-timeout.js +51 -11
- package/dist/fs/atomic-write.js +5 -2
- package/dist/fs/boot-identity.d.ts +0 -4
- package/dist/fs/boot-identity.js +9 -3
- package/dist/fs/directory-registry.js +2 -2
- package/dist/fs/directory-running-lock.js +9 -4
- package/dist/fs/directory-sync.js +2 -3
- package/dist/fs/directory-user-config.d.ts +9 -0
- package/dist/fs/directory-user-config.js +13 -6
- package/dist/fs/directory-workspace.js +11 -14
- package/dist/fs/durable-json.js +22 -16
- package/dist/fs/errno.d.ts +9 -0
- package/dist/fs/errno.js +21 -0
- package/dist/fs/file-lock.d.ts +1 -0
- package/dist/fs/file-lock.js +17 -19
- package/dist/fs/guarded-reader.d.ts +22 -1
- package/dist/fs/guarded-reader.js +85 -15
- package/dist/fs/index.d.ts +1 -0
- package/dist/fs/index.js +1 -0
- package/dist/fs/user-data-directories.js +5 -7
- package/dist/fs/workspace-directory.d.ts +7 -1
- package/dist/fs/workspace-directory.js +9 -2
- package/dist/fs.js +2 -1
- package/dist/http/guarded/client.js +2 -1
- package/dist/http.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mail/builtin-providers.js +4 -2
- package/dist/mail/feedback.js +1 -1
- package/dist/mail/prepare-sender-email.js +7 -1
- package/dist/mail/provider.js +14 -8
- package/dist/mail/rate-limit.js +3 -1
- package/dist/messages.js +1 -1
- package/dist/net/endpoint-policy.js +15 -7
- package/dist/net/index.d.ts +1 -0
- package/dist/net/index.js +1 -0
- package/dist/net/local-http-probe.d.ts +16 -0
- package/dist/net/local-http-probe.js +36 -0
- package/dist/object-store/local-fs.js +3 -1
- package/dist/process.js +10 -9
- package/dist/proxy-env.js +22 -14
- 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.js +14 -3
- package/dist/secrets/credential-sets/vault-archive.d.ts +41 -0
- package/dist/secrets/credential-sets/vault-archive.js +64 -0
- package/dist/secrets/credential-sets/vault.meta.js +6 -2
- package/dist/secrets/credential-sets/vault.repo.memory.js +3 -5
- package/dist/secrets/credential-sets/vault.service.js +22 -17
- package/dist/secrets/credential-sets/vault.types.d.ts +6 -0
- package/dist/secrets/credential-sets/vault.types.js +7 -0
- package/dist/secrets/credential-token.js +9 -2
- package/dist/secrets/keyring.env.js +2 -4
- package/dist/shell.js +2 -1
- package/dist/terminal.js +4 -7
- package/dist/toolchain.js +4 -9
- package/dist/web/first-distinct.d.ts +11 -0
- package/dist/web/first-distinct.js +16 -0
- package/dist/web/html-document.js +35 -39
- package/dist/web/html-to-markdown.js +20 -9
- package/dist/web/web-page.js +28 -25
- package/dist/windows-system-tools.d.ts +15 -0
- package/dist/windows-system-tools.js +16 -0
- package/package.json +10 -6
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { windowsSystemTool } from "../windows-system-tools.js";
|
|
5
|
+
import { fallbackIfMissing } from "./errno.js";
|
|
4
6
|
/** Electron-free OS directory rules. Windows uses roaming app-data and the redirected Personal
|
|
5
7
|
* known folder; Linux honors XDG user-dirs. No directories are created by this resolver.
|
|
6
8
|
* env-paths is intentionally not used: its Windows data directory is LocalAppData/<app>/Data.
|
|
@@ -43,21 +45,17 @@ export function readOptionalText({ file }, _options = {}) {
|
|
|
43
45
|
return fs.readFileSync(file, "utf8");
|
|
44
46
|
}
|
|
45
47
|
catch (error) {
|
|
46
|
-
|
|
47
|
-
return undefined;
|
|
48
|
-
throw error;
|
|
48
|
+
return fallbackIfMissing(error, undefined);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
function readWindowsDocuments() {
|
|
52
|
-
|
|
53
|
-
const output = execFileSync("reg.exe", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", "/v", "Personal"], { encoding: "utf8", timeout: 3000, windowsHide: true });
|
|
52
|
+
const output = execFileSync(windowsSystemTool({ relativePath: ["reg.exe"] }), ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", "/v", "Personal"], { encoding: "utf8", timeout: 3000, windowsHide: true });
|
|
54
53
|
return /Personal\s+REG_(?:EXPAND_)?SZ\s+(.+)/.exec(output)?.[1]?.trim();
|
|
55
54
|
}
|
|
56
55
|
/** OneDrive publishes account mount paths in HKCU; absent accounts are a normal empty result. */
|
|
57
56
|
export function readWindowsOneDriveRoots(_required, _options = {}) {
|
|
58
57
|
try {
|
|
59
|
-
|
|
60
|
-
const output = execFileSync("reg.exe", ["query", "HKCU\\Software\\Microsoft\\OneDrive\\Accounts", "/s", "/v", "UserFolder"], { encoding: "utf8", timeout: 3000, windowsHide: true });
|
|
58
|
+
const output = execFileSync(windowsSystemTool({ relativePath: ["reg.exe"] }), ["query", "HKCU\\Software\\Microsoft\\OneDrive\\Accounts", "/s", "/v", "UserFolder"], { encoding: "utf8", timeout: 3000, windowsHide: true });
|
|
61
59
|
return [...output.matchAll(/UserFolder\s+REG_(?:EXPAND_)?SZ\s+([^\r\n]+)/g)].map((match) => match[1].trim());
|
|
62
60
|
}
|
|
63
61
|
catch (error) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { DirectoryRecord } from "./directory-registry.js";
|
|
2
2
|
/** Shared selection policy for CLI/dev hosts. A registry ID disambiguates adopted paths;
|
|
3
3
|
* a basename selects the home first. No initialized home site preserves the supplied fallback.
|
|
4
|
+
* Hosts may probe availability to resolve an ambiguous name to its sole available directory.
|
|
5
|
+
* Offline records and explicit ID selection are preserved; probe failures propagate.
|
|
4
6
|
* @complexity O(n) registered/child directories and validation probes.
|
|
5
7
|
*/
|
|
6
8
|
export declare function selectWorkspaceDirectory({ home, entries, fallback, selected, roots, listDirectories, isInitialized }: {
|
|
@@ -15,5 +17,9 @@ export declare function selectWorkspaceDirectory({ home, entries, fallback, sele
|
|
|
15
17
|
isInitialized: (required: {
|
|
16
18
|
path: string;
|
|
17
19
|
}) => boolean;
|
|
18
|
-
},
|
|
20
|
+
}, { isAvailable }?: {
|
|
21
|
+
isAvailable?: (required: {
|
|
22
|
+
path: string;
|
|
23
|
+
}) => boolean;
|
|
24
|
+
}): string;
|
|
19
25
|
//# sourceMappingURL=workspace-directory.d.ts.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
/** Shared selection policy for CLI/dev hosts. A registry ID disambiguates adopted paths;
|
|
3
3
|
* a basename selects the home first. No initialized home site preserves the supplied fallback.
|
|
4
|
+
* Hosts may probe availability to resolve an ambiguous name to its sole available directory.
|
|
5
|
+
* Offline records and explicit ID selection are preserved; probe failures propagate.
|
|
4
6
|
* @complexity O(n) registered/child directories and validation probes.
|
|
5
7
|
*/
|
|
6
|
-
export function selectWorkspaceDirectory({ home, entries, fallback, selected, roots = [home], listDirectories, isInitialized },
|
|
8
|
+
export function selectWorkspaceDirectory({ home, entries, fallback, selected, roots = [home], listDirectories, isInitialized }, { isAvailable } = {}) {
|
|
7
9
|
if (selected !== undefined) {
|
|
8
10
|
const byId = entries.find((entry) => entry.id === selected);
|
|
9
11
|
if (byId)
|
|
@@ -13,8 +15,13 @@ export function selectWorkspaceDirectory({ home, entries, fallback, selected, ro
|
|
|
13
15
|
return inHome;
|
|
14
16
|
const matches = [...new Set([...entries.filter((entry) => path.basename(entry.path) === selected).map((entry) => entry.path),
|
|
15
17
|
...roots.filter((root) => root !== home).map((root) => path.join(root, selected)).filter((dir) => isInitialized({ path: dir }))])];
|
|
16
|
-
if (matches.length > 1)
|
|
18
|
+
if (matches.length > 1) {
|
|
19
|
+
// A stale scratch catalog row must not block the sole live site or choose by row order.
|
|
20
|
+
const available = matches.filter(dir => isAvailable?.({ path: dir }) ?? true);
|
|
21
|
+
if (available.length === 1)
|
|
22
|
+
return available[0];
|
|
17
23
|
throw new Error("More than one registered directory has this name. Select its registry ID.");
|
|
24
|
+
}
|
|
18
25
|
return matches[0] ?? path.join(path.dirname(fallback), selected);
|
|
19
26
|
}
|
|
20
27
|
const candidates = [...entries.filter((entry) => path.dirname(entry.path) === home).sort((a, b) => (b.lastOpened ? Date.parse(b.lastOpened) : 0) - (a.lastOpened ? Date.parse(a.lastOpened) : 0)).map((entry) => entry.path), ...roots.flatMap((root) => listDirectories({ root }))];
|
package/dist/fs.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Keeps private `errorCode` / `errorMessage` copies so it owns no cross-module
|
|
9
9
|
* runtime surface.
|
|
10
10
|
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { copyFile, mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
|
12
13
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
13
14
|
/** @internal Extract a Node `error.code` as a string, or `null` when the value carries no code. */
|
|
@@ -72,7 +73,7 @@ export async function atomicCopyFile(sourcePath, destinationPath, options = {})
|
|
|
72
73
|
if (existing != null && options.overwrite !== true) {
|
|
73
74
|
throw destinationExistsError(destination);
|
|
74
75
|
}
|
|
75
|
-
const tempPath = join(destinationDir, `.${basename(destination)}.${process.pid}.${Date.now().toString(36)}.${
|
|
76
|
+
const tempPath = join(destinationDir, `.${basename(destination)}.${process.pid}.${Date.now().toString(36)}.${randomUUID()}.tmp`);
|
|
76
77
|
try {
|
|
77
78
|
await copyFile(source, tempPath);
|
|
78
79
|
// Rename replaces the old file atomically. Removing it first opens a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Policy refusals use EgressRefusedError; DNS, connection and transport failures retain their
|
|
2
2
|
// own errors because they are operational failures, not decisions that a different URL fixes.
|
|
3
3
|
import { classifyIpAddress } from "../../net/address.js";
|
|
4
|
+
import { effectivePort } from "../../core/effective-port.js";
|
|
4
5
|
import { FetchTimeoutError } from "../../fetch-with-timeout.js";
|
|
5
6
|
import { isIP } from "node:net";
|
|
6
7
|
import { EgressRefusedError } from "./errors.js";
|
|
@@ -55,7 +56,7 @@ function assertNoPrivateAddress(hostname, addresses) {
|
|
|
55
56
|
}
|
|
56
57
|
async function resolvePinnedPeer(url, policy, dns) {
|
|
57
58
|
assertAllowedTarget(url, policy);
|
|
58
|
-
const port =
|
|
59
|
+
const port = effectivePort({ url });
|
|
59
60
|
const hostname = stripIpv6Brackets(url.hostname);
|
|
60
61
|
const isDevAllowlisted = policy.devHostAllowlist.includes(hostname);
|
|
61
62
|
const addresses = await resolveHostAddresses(url.hostname, dns);
|
package/dist/http.js
CHANGED
|
@@ -43,6 +43,7 @@ export async function waitForHttpOk(url, { timeoutMs = 20000 } = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
await sleep(150);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
const detail = lastError ? ` (${lastError.message})` : "";
|
|
47
|
+
throw new Error(`timed out waiting for ${url}${detail}`);
|
|
47
48
|
}
|
|
48
49
|
//# sourceMappingURL=http.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { atomicCopyFile, pathContains, readLogTail, removePathBestEffort } from
|
|
|
43
43
|
export type { HttpWaitOptions } from "./http.js";
|
|
44
44
|
export { waitForHttpOk } from "./http.js";
|
|
45
45
|
export type { DeadlineSignalOptions, FetchWithTimeoutOptions } from "./fetch-with-timeout.js";
|
|
46
|
-
export { FETCH_TIMEOUT_MS, FetchTimeoutError, createDeadlineSignal, fetchWithTimeout } from "./fetch-with-timeout.js";
|
|
46
|
+
export { FETCH_TIMEOUT_MS, FetchTimeoutError, ResponseLimitExceededError, createDeadlineSignal, fetchWithTimeout } from "./fetch-with-timeout.js";
|
|
47
47
|
export type { WellKnownUserToolchainOptions } from "./toolchain.js";
|
|
48
48
|
export { wellKnownUserToolchainBins } from "./toolchain.js";
|
|
49
49
|
export type { BufferedCommandResult } from "./shell.js";
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export { mergeProxyAwareEnv, parseMacosScutilProxyOutput, parseWindowsInternetSe
|
|
|
37
37
|
export { collectProcessTreePids, createProcessStampArgs, isProcessAlive, listProcessSnapshots, listProcessUsage, matchesProcessStamp, matchesStampedProcess, readFlagValue, readProcessStamp, readProcessStampFromCommand, spawnBackgroundProcess, spawnLoggedProcess, stopProcesses, waitForProcessExit, } from "./process.js";
|
|
38
38
|
export { atomicCopyFile, pathContains, readLogTail, removePathBestEffort } from "./fs.js";
|
|
39
39
|
export { waitForHttpOk } from "./http.js";
|
|
40
|
-
export { FETCH_TIMEOUT_MS, FetchTimeoutError, createDeadlineSignal, fetchWithTimeout } from "./fetch-with-timeout.js";
|
|
40
|
+
export { FETCH_TIMEOUT_MS, FetchTimeoutError, ResponseLimitExceededError, createDeadlineSignal, fetchWithTimeout } from "./fetch-with-timeout.js";
|
|
41
41
|
export { wellKnownUserToolchainBins } from "./toolchain.js";
|
|
42
42
|
export { execCommandViaLoginShell, execFileBuffered } from "./shell.js";
|
|
43
43
|
export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createAssetCacheDispatcher, createValidatingLookup, isCacheableExternalUrl, } from "./asset-cache.js";
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `hosted: false`, so it can never make a site look configured.
|
|
7
7
|
*/
|
|
8
8
|
import { nowIso } from "@jini-ai/core/primitives";
|
|
9
|
+
import { effectivePort } from "../core/effective-port.js";
|
|
9
10
|
import { SmtpMailerAdapter } from "./smtp.js";
|
|
10
11
|
/**
|
|
11
12
|
* SMTP endpoint packed into a credential `baseUrl` (`https://host:port`); 443/465 = implicit TLS.
|
|
@@ -13,7 +14,7 @@ import { SmtpMailerAdapter } from "./smtp.js";
|
|
|
13
14
|
*/
|
|
14
15
|
export function parseSmtpEndpoint({ baseUrl }) {
|
|
15
16
|
const url = new URL(baseUrl);
|
|
16
|
-
const port =
|
|
17
|
+
const port = effectivePort({ url });
|
|
17
18
|
return { host: url.hostname, port, secure: port === 443 || port === 465 };
|
|
18
19
|
}
|
|
19
20
|
export function createSmtpMailProvider(required) {
|
|
@@ -39,7 +40,8 @@ export function createConsoleMailProvider(required, optional = {}) {
|
|
|
39
40
|
// The body is the point of a local mailer: a sign-in or confirmation link must be usable from the
|
|
40
41
|
// log, since nothing is delivered. Text is preferred (readable); an html-only message shows its html.
|
|
41
42
|
const body = message.text ?? message.html;
|
|
42
|
-
|
|
43
|
+
const bodySuffix = body === undefined ? "" : `\n${body}`;
|
|
44
|
+
log(`[mail:console] to=${message.to.email} subject=${JSON.stringify(message.subject)}${bodySuffix}`);
|
|
43
45
|
return { ok: true, providerMessageId: `console-${counter}`, acceptedAt: nowIso({ clock: required.clock }) };
|
|
44
46
|
},
|
|
45
47
|
async sendBatch({ messages, ...delivery }) {
|
package/dist/mail/feedback.js
CHANGED
|
@@ -37,7 +37,7 @@ export function createMailFeedbackIngest(required, optional = {}) {
|
|
|
37
37
|
const apply = async (input) => {
|
|
38
38
|
const outcome = await required.inbox.record(input);
|
|
39
39
|
if (outcome === "duplicate")
|
|
40
|
-
return
|
|
40
|
+
return "duplicate";
|
|
41
41
|
const scope = suppressionScopeFor({ event: input.event });
|
|
42
42
|
if (scope !== null) {
|
|
43
43
|
const reason = input.event.kind === "complained" ? "complained" : "bounced";
|
|
@@ -54,7 +54,7 @@ export async function prepareSenderEmail(required, _optional = {}) {
|
|
|
54
54
|
validateEmailSourceTemplate({ fields: source.fields, template });
|
|
55
55
|
const resolved = await sender.resolveEvent({ ...context, eventId });
|
|
56
56
|
// MOVE: Compare against the latest activation so re-enabling cannot revive a backlog.
|
|
57
|
-
if (!
|
|
57
|
+
if (!occurredSinceActivation({ occurredAt: resolved.occurredAt, activatedAt: binding.activatedAt }))
|
|
58
58
|
throw new EmailSenderError({ code: "configuration" });
|
|
59
59
|
const to = recipientAddress({ source, resolved, recipientId: binding.recipientId });
|
|
60
60
|
const { values, warnings } = templateValues({ fields: source.fields, tokens: template.fields, values: resolved.values });
|
|
@@ -65,4 +65,10 @@ export async function prepareSenderEmail(required, _optional = {}) {
|
|
|
65
65
|
replyTo: { email: binding.replyTo }, subject: rendered.subject, html: rendered.html, text: rendered.text,
|
|
66
66
|
purpose: source.purpose, lane: source.lane, warnings };
|
|
67
67
|
}
|
|
68
|
+
/** True only for an event at or after the binding's activation; an unparsable time is never current. */
|
|
69
|
+
function occurredSinceActivation({ occurredAt, activatedAt }) {
|
|
70
|
+
if (!activatedAt)
|
|
71
|
+
return false;
|
|
72
|
+
return Date.parse(occurredAt) >= Date.parse(activatedAt);
|
|
73
|
+
}
|
|
68
74
|
//# sourceMappingURL=prepare-sender-email.js.map
|
package/dist/mail/provider.js
CHANGED
|
@@ -64,14 +64,20 @@ function laneStatus(required) {
|
|
|
64
64
|
export function computeMailStatus(required, optional = {}) {
|
|
65
65
|
const { setting, registry, hasCredential } = required;
|
|
66
66
|
const lanes = ["transactional", "bulk"].map((lane) => laneStatus({ lane, providerId: setting[lane], registry, hasCredential }));
|
|
67
|
-
const
|
|
68
|
-
const
|
|
67
|
+
const senderWarning = senderDomainWarning(optional.senderDomainState);
|
|
68
|
+
const warnings = senderWarning === undefined ? [] : [senderWarning];
|
|
69
|
+
return { state: overallMailState(lanes), lanes, warnings };
|
|
70
|
+
}
|
|
71
|
+
function senderDomainWarning(domain) {
|
|
69
72
|
if (domain === null)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
return "sender_unknown";
|
|
74
|
+
if (domain === undefined || domain === "verified")
|
|
75
|
+
return undefined;
|
|
76
|
+
return "sender_unverified";
|
|
77
|
+
}
|
|
78
|
+
function overallMailState(lanes) {
|
|
79
|
+
if (lanes.every((status) => status.state === "ready"))
|
|
80
|
+
return "ready";
|
|
81
|
+
return lanes[0].state === "unconfigured" ? "unconfigured" : "degraded";
|
|
76
82
|
}
|
|
77
83
|
//# sourceMappingURL=provider.js.map
|
package/dist/mail/rate-limit.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export function createMailLaneLimiter(required, optional = {}) {
|
|
9
9
|
const { perSecond, burst } = required.limit;
|
|
10
|
-
|
|
10
|
+
// Positive comparisons on purpose: NaN fails both and is rejected.
|
|
11
|
+
const validLimit = perSecond > 0 && burst >= 1;
|
|
12
|
+
if (!validLimit)
|
|
11
13
|
throw new RangeError("mail rate limit needs perSecond > 0 and burst >= 1");
|
|
12
14
|
const now = optional.now ?? Date.now;
|
|
13
15
|
// At least one token whenever burst > 1: 20% of a small burst (Resend's 2) floors to 0, which let
|
package/dist/messages.js
CHANGED
|
@@ -5,7 +5,7 @@ export const defaultPlatformMessages = {
|
|
|
5
5
|
atomicSymlinkRefused: () => "atomic write refused a symbolic-link destination",
|
|
6
6
|
atomicPermissionsRefused: () => "atomic write refused non-owner-only permissions",
|
|
7
7
|
atomicJsonUnserializable: () => "atomic JSON data must be serializable",
|
|
8
|
-
fileLockIntervalsInvalid: () => "file lock intervals must be positive safe integers (timeout may be zero; stale may be Infinity)",
|
|
8
|
+
fileLockIntervalsInvalid: () => "file lock intervals must be positive safe integers (timeout may be zero or Infinity; stale may be Infinity)",
|
|
9
9
|
fileLockWriteNoProgress: () => "file lock ownership write made no progress",
|
|
10
10
|
egressRefused: () => "egress to the requested host was refused by the outbound network policy",
|
|
11
11
|
rootKeySealedWarning: ({ subject }) => `IMPORTANT: anything sealed while ${subject} was in place was sealed under key material derived from these same bytes, `,
|
|
@@ -13,7 +13,13 @@ function normalizeBracketedIpv6(hostname) {
|
|
|
13
13
|
// form, so `localhost.` must normalize to `localhost` before the equality
|
|
14
14
|
// check below — and `0.0.0.0.`, `10.0.0.1.`, etc. must normalize before
|
|
15
15
|
// the IPv4 classifier parses them. Strips one or more trailing dots.
|
|
16
|
-
|
|
16
|
+
// A scan rather than /\.+$/, which backtracks quadratically on long interior dot runs. This module
|
|
17
|
+
// stays import-free (see its contract test), so it does not use core's trimCharsEnd.
|
|
18
|
+
const lowered = stripped.toLowerCase();
|
|
19
|
+
let end = lowered.length;
|
|
20
|
+
while (end > 0 && lowered[end - 1] === '.')
|
|
21
|
+
end -= 1;
|
|
22
|
+
return lowered.slice(0, end);
|
|
17
23
|
}
|
|
18
24
|
function parseIpv4(hostname) {
|
|
19
25
|
const parts = hostname.split('.');
|
|
@@ -71,7 +77,7 @@ export function expandIpv6({ address: addr }) {
|
|
|
71
77
|
const fill = 8 - left.length - right.length;
|
|
72
78
|
if (fill < 1)
|
|
73
79
|
return null;
|
|
74
|
-
groups = [...left, ...
|
|
80
|
+
groups = [...left, ...Array.from({ length: fill }, () => 0), ...right];
|
|
75
81
|
}
|
|
76
82
|
if (!validIpv6Groups(groups))
|
|
77
83
|
return null;
|
|
@@ -88,8 +94,8 @@ function foldIpv4Tail(s) {
|
|
|
88
94
|
if (o.length !== 4 || o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
89
95
|
return null;
|
|
90
96
|
// `o` is now a validated 4-element array — indices 0-3 always defined.
|
|
91
|
-
const hi = ((
|
|
92
|
-
const lo = ((
|
|
97
|
+
const hi = ((o[0] << 8) | o[1]).toString(16);
|
|
98
|
+
const lo = ((o[2] << 8) | o[3]).toString(16);
|
|
93
99
|
s = `${v4[1]}${hi}:${lo}`;
|
|
94
100
|
}
|
|
95
101
|
return s;
|
|
@@ -116,9 +122,11 @@ function classifyIpv4Bytes(bytes) {
|
|
|
116
122
|
function isPrivateIpv4Bytes([a, b]) {
|
|
117
123
|
if (a === 100)
|
|
118
124
|
return b >= 64 && b <= 127;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
if (a === 172)
|
|
126
|
+
return b >= 16 && b <= 31;
|
|
127
|
+
if (a === 192)
|
|
128
|
+
return b === 168;
|
|
129
|
+
return a === 10;
|
|
122
130
|
}
|
|
123
131
|
// Each /96 prefix carries IPv4 in the final four bytes. Matching canonical
|
|
124
132
|
// bytes also handles expanded IPv6 and WHATWG URL's dotted-to-hex normalization.
|
package/dist/net/index.d.ts
CHANGED
package/dist/net/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe a loopback service once, accepting only HTTP 200 without following redirects.
|
|
3
|
+
* @param required Absolute HTTP(S) URL. Non-loopback targets are refused before connecting.
|
|
4
|
+
* @param options Optional expected leaf PEM, TLS server name and request deadline (default 1500ms).
|
|
5
|
+
* @returns False for transport, TLS, timeout or non-200 failures; true for an authenticated 200.
|
|
6
|
+
* @throws For malformed URLs, certificate PEM or invalid timeout values (configuration errors).
|
|
7
|
+
* @complexity O(c) time/space for c certificate bytes; one bounded network request.
|
|
8
|
+
*/
|
|
9
|
+
export declare function probeLoopbackHttp({ url }: {
|
|
10
|
+
url: string;
|
|
11
|
+
}, { certificate, servername, timeoutMs }?: {
|
|
12
|
+
certificate?: string | Buffer | undefined;
|
|
13
|
+
servername?: string | undefined;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}): Promise<boolean>;
|
|
16
|
+
//# sourceMappingURL=local-http-probe.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { X509Certificate } from 'node:crypto';
|
|
2
|
+
import { get as httpGet } from 'node:http';
|
|
3
|
+
import { get as httpsGet } from 'node:https';
|
|
4
|
+
import { isLoopbackHostname } from './address.js';
|
|
5
|
+
/**
|
|
6
|
+
* Probe a loopback service once, accepting only HTTP 200 without following redirects.
|
|
7
|
+
* @param required Absolute HTTP(S) URL. Non-loopback targets are refused before connecting.
|
|
8
|
+
* @param options Optional expected leaf PEM, TLS server name and request deadline (default 1500ms).
|
|
9
|
+
* @returns False for transport, TLS, timeout or non-200 failures; true for an authenticated 200.
|
|
10
|
+
* @throws For malformed URLs, certificate PEM or invalid timeout values (configuration errors).
|
|
11
|
+
* @complexity O(c) time/space for c certificate bytes; one bounded network request.
|
|
12
|
+
*/
|
|
13
|
+
export function probeLoopbackHttp({ url }, { certificate, servername, timeoutMs = 1500 } = {}) {
|
|
14
|
+
const target = new URL(url);
|
|
15
|
+
if (!isLoopbackHostname({ hostname: target.hostname }))
|
|
16
|
+
return Promise.resolve(false);
|
|
17
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:')
|
|
18
|
+
return Promise.resolve(false);
|
|
19
|
+
const hostname = target.hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '');
|
|
20
|
+
// Only the configured leaf is a trust anchor, never an issuer bundled after it.
|
|
21
|
+
// Partial-chain trust permits mkcert leaves without installing their CA into this Node process.
|
|
22
|
+
const trust = certificate === undefined ? {} : { ca: new X509Certificate(certificate).toString(), allowPartialTrustChain: true };
|
|
23
|
+
const options = {
|
|
24
|
+
agent: false, signal: AbortSignal.timeout(timeoutMs), rejectUnauthorized: true,
|
|
25
|
+
// Do not let a hosts-file/DNS override turn the localhost exception into remote trust.
|
|
26
|
+
hostname: hostname.replace(/^localhost$/, '127.0.0.1'),
|
|
27
|
+
servername: servername ?? (hostname === 'localhost' ? 'localhost' : undefined),
|
|
28
|
+
...trust,
|
|
29
|
+
};
|
|
30
|
+
return new Promise(resolve => {
|
|
31
|
+
const receive = (response) => { response.resume(); resolve(response.statusCode === 200); };
|
|
32
|
+
const get = target.protocol === 'https:' ? httpsGet : httpGet;
|
|
33
|
+
get(target, options, receive).once('error', () => resolve(false));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=local-http-probe.js.map
|
|
@@ -29,7 +29,9 @@ function contained(required) {
|
|
|
29
29
|
const { rootDir, key } = required;
|
|
30
30
|
const path = join(rootDir, key);
|
|
31
31
|
const rel = relative(resolve(rootDir), resolve(path));
|
|
32
|
-
|
|
32
|
+
const invalidKey = key === '' || isAbsolute(key);
|
|
33
|
+
const escapesRoot = rel === '' || rel.startsWith('..') || isAbsolute(rel);
|
|
34
|
+
if (invalidKey || escapesRoot) {
|
|
33
35
|
throw new Error(`object key '${key}' is outside the store root`);
|
|
34
36
|
}
|
|
35
37
|
return path;
|
package/dist/process.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
import { execFile, spawn } from "node:child_process";
|
|
14
14
|
import { posix, win32 } from "node:path";
|
|
15
15
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
16
|
+
import { captureLineTail } from "@jini-ai/core/text";
|
|
17
|
+
import { windowsPowerShell } from "./windows-system-tools.js";
|
|
16
18
|
import { createCommandInvocation } from "./command.js";
|
|
17
19
|
import { execFileBuffered } from "./shell.js";
|
|
18
20
|
/** @internal Extract a Node `error.code` as a string, or `null` when the value carries no code. */
|
|
@@ -195,9 +197,7 @@ export function isProcessAlive(pid) {
|
|
|
195
197
|
return true;
|
|
196
198
|
}
|
|
197
199
|
catch (error) {
|
|
198
|
-
|
|
199
|
-
return false;
|
|
200
|
-
return true;
|
|
200
|
+
return errorCode(error) !== "ESRCH";
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
/**
|
|
@@ -221,10 +221,12 @@ function parsePsOutput(stdout) {
|
|
|
221
221
|
return stdout
|
|
222
222
|
.split(/\r?\n/)
|
|
223
223
|
.map((line) => {
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
// Same as /^\s*(\d+)\s+(\d+)\s+(.+)$/, split so the command tail cannot backtrack.
|
|
225
|
+
const match = /^\s*(\d+)\s+(\d+)/.exec(line);
|
|
226
|
+
const command = match ? captureLineTail({ rest: line.slice(match[0].length) }, { minWhitespace: 1 }) : null;
|
|
227
|
+
if (!match || command === null)
|
|
226
228
|
return null;
|
|
227
|
-
return { pid: Number(match[1]), ppid: Number(match[2]), command
|
|
229
|
+
return { pid: Number(match[1]), ppid: Number(match[2]), command };
|
|
228
230
|
})
|
|
229
231
|
.filter((snapshot) => snapshot != null);
|
|
230
232
|
}
|
|
@@ -248,8 +250,7 @@ async function listWindowsProcessSnapshots() {
|
|
|
248
250
|
"Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, CommandLine | ConvertTo-Json -Compress",
|
|
249
251
|
].join("; ");
|
|
250
252
|
const stdout = await new Promise((resolveList, rejectList) => {
|
|
251
|
-
|
|
252
|
-
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], { encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }, (error, out) => {
|
|
253
|
+
execFile(windowsPowerShell(), ["-NoProfile", "-NonInteractive", "-Command", command], { encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }, (error, out) => {
|
|
253
254
|
if (error)
|
|
254
255
|
rejectList(error);
|
|
255
256
|
else
|
|
@@ -341,7 +342,7 @@ function parseCimUsage(stdout) {
|
|
|
341
342
|
/** @internal Fixed executable and argv, invoked directly without a command shell. */
|
|
342
343
|
function processUsageCommand(windows) {
|
|
343
344
|
return windows
|
|
344
|
-
? { command:
|
|
345
|
+
? { command: windowsPowerShell(), args: ["-NoProfile", "-NonInteractive", "-Command",
|
|
345
346
|
"$ErrorActionPreference = 'Stop'; Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, WorkingSetSize, UserModeTime, KernelModeTime, Name | ConvertTo-Json -Compress"] }
|
|
346
347
|
: { command: "ps", args: ["-axo", "pid=,ppid=,rss=,time=,comm="] };
|
|
347
348
|
}
|
package/dist/proxy-env.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* proxy-config tools; owns no other sibling dependency.
|
|
12
12
|
*/
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { captureLineTail } from "@jini-ai/core/text";
|
|
14
15
|
const CANONICAL_PROXY_ENV_KEYS = new Map([
|
|
15
16
|
["all_proxy", "ALL_PROXY"],
|
|
16
17
|
["http_proxy", "HTTP_PROXY"],
|
|
@@ -78,18 +79,21 @@ function collectSourceProxyEntries(input) {
|
|
|
78
79
|
if (value == null)
|
|
79
80
|
continue;
|
|
80
81
|
const canonicalKey = canonicalProxyEnvKey(key);
|
|
81
|
-
if (
|
|
82
|
+
if (canonicalKey)
|
|
83
|
+
keepPreferredProxyEntry(proxyEntries, { canonicalKey, key, value });
|
|
84
|
+
else
|
|
82
85
|
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
86
|
}
|
|
91
87
|
return proxyEntries;
|
|
92
88
|
}
|
|
89
|
+
/** @internal Record `key`'s value unless a lowercase spelling of the same variable already won. */
|
|
90
|
+
function keepPreferredProxyEntry(proxyEntries, entry) {
|
|
91
|
+
const current = proxyEntries.get(entry.canonicalKey);
|
|
92
|
+
const preferLowercase = entry.key === entry.key.toLowerCase();
|
|
93
|
+
if (!current || preferLowercase || !current.preferLowercase) {
|
|
94
|
+
proxyEntries.set(entry.canonicalKey, { preferLowercase, value: entry.value });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
93
97
|
/** @internal Whether `env` already contains any case-variant of the given canonical proxy key. */
|
|
94
98
|
function hasCanonicalProxyEnv(env, canonicalKey) {
|
|
95
99
|
return Object.keys(env).some((key) => key.toLowerCase() === canonicalKey.toLowerCase());
|
|
@@ -249,16 +253,20 @@ function parseScutilLines(stdout) {
|
|
|
249
253
|
}
|
|
250
254
|
/** @internal Record one `N : value` ExceptionsList entry; returns whether the array is still open (false on `}`). */
|
|
251
255
|
function collectScutilException(line, exceptions) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
// Same as /^\d+\s*:\s*(.+)$/, split so the value tail cannot backtrack.
|
|
257
|
+
const index = /^\d+\s*:/.exec(line);
|
|
258
|
+
const value = index ? captureLineTail({ rest: line.slice(index[0].length) }) : null;
|
|
259
|
+
if (value !== null)
|
|
260
|
+
exceptions.push(value.trim());
|
|
255
261
|
return line !== "}";
|
|
256
262
|
}
|
|
257
263
|
/** @internal Record one top-level `Name : value` scutil scalar, ignoring lines of any other shape. */
|
|
258
264
|
function collectScutilScalar(line, scalars) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
// Same as /^([A-Za-z][A-Za-z0-9]*)\s*:\s*(.+)$/, split so the value tail cannot backtrack.
|
|
266
|
+
const name = /^([A-Za-z][A-Za-z0-9]*)\s*:/.exec(line);
|
|
267
|
+
const value = name ? captureLineTail({ rest: line.slice(name[0].length) }) : null;
|
|
268
|
+
if (name && value !== null)
|
|
269
|
+
scalars.set(name[1], value.trim());
|
|
262
270
|
}
|
|
263
271
|
/** @internal The `<prefix>Proxy`/`<prefix>Port` URL when `<prefix>Enable` is `1`, else `null`. */
|
|
264
272
|
function scutilEnabledProxy(input) {
|
|
@@ -4,5 +4,6 @@ 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 { captureVaultArchive, prepareVaultArchiveRestore, type VaultArchiveEntry } from "./vault-archive.js";
|
|
7
8
|
export { matchingCredentialProviderIds, selectCredentialForProvider, suggestCredentialWorksFor, normalizeCredentialWorksFor, type CredentialProviderDescriptor, type MatchableCredential } from "./provider-matches.js";
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,5 +3,6 @@ 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 { captureVaultArchive, prepareVaultArchiveRestore } from "./vault-archive.js";
|
|
6
7
|
export { matchingCredentialProviderIds, selectCredentialForProvider, suggestCredentialWorksFor, normalizeCredentialWorksFor } from "./provider-matches.js";
|
|
7
8
|
//# sourceMappingURL=index.js.map
|
|
@@ -23,15 +23,26 @@ export function normalizeCredentialWorksFor({ value }, _options = {}) {
|
|
|
23
23
|
*/
|
|
24
24
|
export function matchingCredentialProviderIds({ credential, providers }, _options = {}) {
|
|
25
25
|
const { credentialType, worksFor } = credential.meta;
|
|
26
|
-
if ((
|
|
27
|
-
(
|
|
28
|
-
!Array.isArray(worksFor) || Array.from(worksFor).some(id => typeof id !== "string" || !id || id !== id.toLowerCase() || /\s/.test(id)))
|
|
26
|
+
if (!hasMatchableType({ kind: credential.kind, credentialType }) ||
|
|
27
|
+
!Array.isArray(worksFor) || !Array.from(worksFor).every(isCanonicalProviderId))
|
|
29
28
|
return [];
|
|
30
29
|
const ids = new Set(worksFor);
|
|
31
30
|
return providers.filter(provider => (credentialType !== "custom" || provider.acceptsCustomToken) &&
|
|
32
31
|
(ids.has(provider.id.toLowerCase()) || ids.has(provider.vendorId.toLowerCase())))
|
|
33
32
|
.map(provider => provider.id);
|
|
34
33
|
}
|
|
34
|
+
/** vendor/custom rows only; a custom-kind row must also carry the custom subtype. */
|
|
35
|
+
function hasMatchableType({ kind, credentialType }) {
|
|
36
|
+
if (kind !== "vendor" && kind !== "custom")
|
|
37
|
+
return false;
|
|
38
|
+
if (kind === "custom" && credentialType !== "custom")
|
|
39
|
+
return false;
|
|
40
|
+
return credentialType === "vendor" || credentialType === "custom";
|
|
41
|
+
}
|
|
42
|
+
/** A non-empty, already-lowercased ID without whitespace, as {@link normalizeCredentialWorksFor} stores it. */
|
|
43
|
+
function isCanonicalProviderId(id) {
|
|
44
|
+
return typeof id === "string" && id !== "" && id === id.toLowerCase() && !/\s/.test(id);
|
|
45
|
+
}
|
|
35
46
|
/** Select an eligible explicit ID, the descriptor's provider-group default, or a sole candidate.
|
|
36
47
|
* Missing IDs and unresolved ambiguity return null; custom-group defaults do not rank candidates.
|
|
37
48
|
* @complexity O(credentials * average worksFor) time, O(credentials) candidate space.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SecretsVaultService } from "./vault.service.js";
|
|
2
|
+
import { type VaultKind, type VaultMeta, type VaultOptions, type VaultPutRecord, type VaultQuery, type VaultRepoPort } from "./vault.types.js";
|
|
3
|
+
/** Logical transport only: source workspace, root keys, envelopes and revision history never travel. */
|
|
4
|
+
export interface VaultArchiveEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: VaultKind;
|
|
7
|
+
ownerKey: string;
|
|
8
|
+
binding: string | null;
|
|
9
|
+
provider: string | null;
|
|
10
|
+
label: string;
|
|
11
|
+
isDefault: boolean;
|
|
12
|
+
meta: VaultMeta;
|
|
13
|
+
payload: VaultMeta | null;
|
|
14
|
+
}
|
|
15
|
+
/** Capture through the owning service's authorization and parsers. Hosts supply exact scopes and
|
|
16
|
+
* attachment-filtered repositories; this helper never discovers other workspaces or principals.
|
|
17
|
+
* Diagnostics contain only a count, because even labels/IDs may contain pasted secrets.
|
|
18
|
+
* @example const captured = await captureVaultArchive({ vault, repo, queries: [siteScope] });
|
|
19
|
+
* @complexity O(rows + payload bytes), bounded to 2,000 rows by the transport contract.
|
|
20
|
+
*/
|
|
21
|
+
export declare function captureVaultArchive({ vault, queries, repo }: {
|
|
22
|
+
vault: SecretsVaultService;
|
|
23
|
+
queries: readonly VaultQuery[];
|
|
24
|
+
repo?: Pick<VaultRepoPort, "list">;
|
|
25
|
+
}, _options?: VaultOptions): Promise<{
|
|
26
|
+
entries: VaultArchiveEntry[];
|
|
27
|
+
notices: string[];
|
|
28
|
+
}>;
|
|
29
|
+
/** Validate and re-seal every entry before the host atomically commits any of them. Keeping the
|
|
30
|
+
* transaction in the host lets settings and vault rows join the same commit. Never call put in a
|
|
31
|
+
* loop: a bad later entry must leave the recipient completely unchanged.
|
|
32
|
+
* @throws VaultError or the host's policy error; callers expose a fixed optional-import notice.
|
|
33
|
+
* @example const records = await prepareVaultArchiveRestore({ vault, workspaceId, entries });
|
|
34
|
+
* @complexity O(entries + payload bytes), at most 2,000 entries.
|
|
35
|
+
*/
|
|
36
|
+
export declare function prepareVaultArchiveRestore({ vault, workspaceId, entries }: {
|
|
37
|
+
vault: SecretsVaultService;
|
|
38
|
+
workspaceId: string;
|
|
39
|
+
entries: readonly VaultArchiveEntry[];
|
|
40
|
+
}, _options?: VaultOptions): Promise<VaultPutRecord[]>;
|
|
41
|
+
//# sourceMappingURL=vault-archive.d.ts.map
|