@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @jini-ai/platform
|
|
2
2
|
|
|
3
|
+
## 0.6.15
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add optional `now` and opaque per-install `settings` to `MailAdapterCreateContext`, inherited by
|
|
8
|
+
`MailProviderModule.create`. Export `createMailAdapterContext` from `@jini-ai/platform/mail` so hosts
|
|
9
|
+
can supply a live real clock and fresh empty settings at composition time while legacy direct
|
|
10
|
+
create calls remain compatible. Provider modules retain ownership of settings validation.
|
|
11
|
+
|
|
12
|
+
- Add opt-in plugin source suffix exclusions and test-file inclusion. Explicit allow-paths and their ancestors now override name exclusions, while excluded descendants and suffix exclusions still skip. Default packing stays unchanged when the new options and conflicting allow/name rules are absent.
|
|
13
|
+
|
|
14
|
+
Export ResponseLimitExceededError with limitBytes from platform/fetch-with-timeout and the platform root. Guarded response-text overflow throws this RangeError subclass; invalid caps keep plain RangeError and existing truncation behavior is preserved.
|
|
15
|
+
|
|
16
|
+
Accept jsdom 26 and 27 as optional CMS peers alongside 25; media uses DOM APIs shared across these versions.
|
|
17
|
+
|
|
18
|
+
- Add `createDeadlineSignal` and `DeadlineSignalOptions` to `@jini-ai/platform/fetch-with-timeout`
|
|
19
|
+
and the root entry. Compose a tool's remaining wait budget with optional caller cancellation,
|
|
20
|
+
subtract elapsed time through an optional start time and clock port, and clamp remaining time to zero.
|
|
21
|
+
|
|
22
|
+
- `createDeadlineSignal` accepts an optional cancellable `wait({ ms, signal })`
|
|
23
|
+
timer port alongside its existing clock port. It now returns `{ signal, dispose }`;
|
|
24
|
+
rc.1/rc.2 consumers must destructure the signal and call `dispose()` in `finally`.
|
|
25
|
+
Disposal cancels the pending wait and detaches parent listeners without aborting the
|
|
26
|
+
returned signal. Deadline expiry and parent cancellation also release resources;
|
|
27
|
+
parent abort reasons and elapsed-budget accounting are preserved.
|
|
28
|
+
|
|
3
29
|
## 0.6.13
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -14,12 +14,9 @@ that were split out of OD's monolithic daemon (see the archived provenance ledge
|
|
|
14
14
|
npm install @jini-ai/platform
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`undici@^7.25.0` is
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
the synchronous factory loads it on demand and reports a missing-peer error if it is absent.
|
|
21
|
-
The root declarations retain undici types, so TypeScript consumers of those declarations also
|
|
22
|
-
need the peer available. The dispatcher validates DNS at connection time, and cache `close()`
|
|
17
|
+
`undici@^7.25.0` is a regular dependency, installed with this package for the SSRF-safe asset
|
|
18
|
+
cache's default dispatcher. The synchronous factory constructs it on demand and propagates
|
|
19
|
+
construction failures. The dispatcher validates DNS at connection time, and cache `close()`
|
|
23
20
|
closes only its default dispatcher; a host-supplied dispatcher remains owned by the host.
|
|
24
21
|
Interactive terminal sessions (`createTerminalService`)
|
|
25
22
|
need a `PtySpawn` implementation supplied by the caller — this package defines the `PtyProcess`/
|
|
@@ -37,7 +34,8 @@ need a `PtySpawn` implementation supplied by the caller — this package defines
|
|
|
37
34
|
so profile-only `PATH` entries are visible).
|
|
38
35
|
- **Filesystem** — `pathContains`, `atomicCopyFile`, `removePathBestEffort`, `readLogTail`.
|
|
39
36
|
- **HTTP readiness** — `waitForHttpOk`.
|
|
40
|
-
- **
|
|
37
|
+
- **Response byte limits** — `ResponseLimitExceededError` from `@jini-ai/platform/fetch-with-timeout` carries `limitBytes` and extends `RangeError` for guarded response-text overflow.
|
|
38
|
+
- **Deadline signals** — `createDeadlineSignal({ waitMs }, { signal, startedAt, now, wait })` from `@jini-ai/platform/fetch-with-timeout` (also root) returns `{ signal, dispose }`. It subtracts elapsed milliseconds using the optional `now: () => number` clock (default `Date.now`) and combines the remaining budget with caller cancellation. The optional `wait({ ms, signal }): Promise<void>` timer port resolves after the delay and must release its timer and settle when its signal aborts; real timers are the default. Call idempotent `dispose()` in `finally` to cancel the pending wait and detach parent listeners without aborting the returned signal. Deadline expiry uses a `DOMException` named `TimeoutError`; parent abort preserves its reason.
|
|
41
39
|
- **Toolchain discovery** — `wellKnownUserToolchainBins` (npm/pnpm/bun/cargo/deno/go/pyenv,
|
|
42
40
|
asdf/volta/mise/nvm/fnm shims).
|
|
43
41
|
- **Proxy-aware env** — `resolveSystemProxyEnv`, `mergeProxyAwareEnv` (macOS `scutil` / Windows
|
|
@@ -164,7 +162,8 @@ replacement races remain outside this guarantee. Omitting the policy retains exi
|
|
|
164
162
|
`readFetchResponseText({ response }, { maxResponseBytes?, strictUtf8? })`, and
|
|
165
163
|
`fetchTextWithTimeout({ url, timeoutMs, init? }, { fetch?, maxResponseBytes?, strictUtf8? })`.
|
|
166
164
|
Text defaults to unlimited bytes and replacement UTF-8 decoding. Caps must be nonnegative safe
|
|
167
|
-
integers; exactly the cap is accepted, overflow throws `
|
|
165
|
+
integers; exactly the cap is accepted, overflow throws `ResponseLimitExceededError` (a `RangeError`
|
|
166
|
+
with `limitBytes`), invalid caps throw plain `RangeError`, and strict UTF-8 throws on
|
|
168
167
|
malformed/truncated input. Streamed bytes are counted before decoding, independent of Content-Length;
|
|
169
168
|
failed guarded consumption cancels and releases the reader without masking the original error.
|
|
170
169
|
Incoming chunk allocation is controlled by the upstream stream. The text-fetch helper maps timeout
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LookupFunction } from 'node:net';
|
|
2
2
|
import type { Agent } from 'undici';
|
|
3
|
-
/**
|
|
4
|
-
* @throws When the
|
|
3
|
+
/** Construct the dispatcher with platform's required undici dependency; the cache owns lookup validation.
|
|
4
|
+
* @throws When the Agent cannot be constructed.
|
|
5
5
|
*/
|
|
6
6
|
export declare function createUndiciAssetCacheDispatcher({ lookup }: {
|
|
7
7
|
lookup: LookupFunction;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
const require = createRequire(import.meta.url);
|
|
3
|
-
/**
|
|
4
|
-
* @throws When the
|
|
3
|
+
/** Construct the dispatcher with platform's required undici dependency; the cache owns lookup validation.
|
|
4
|
+
* @throws When the Agent cannot be constructed.
|
|
5
5
|
*/
|
|
6
6
|
export function createUndiciAssetCacheDispatcher({ lookup }, _options = {}) {
|
|
7
7
|
const { Agent } = require('undici');
|
package/dist/asset-cache.d.ts
CHANGED
|
@@ -52,9 +52,7 @@ export interface AssetCache {
|
|
|
52
52
|
/** Closes only the default adapter; an injected dispatcher remains owned by its host. */
|
|
53
53
|
close?(): Promise<void>;
|
|
54
54
|
}
|
|
55
|
-
/** Default adapter: validate DNS at connection time, so the checked address is the connected one.
|
|
56
|
-
* @throws {AssetCacheError} When the host has not installed the optional undici peer.
|
|
57
|
-
*/
|
|
55
|
+
/** Default adapter: validate DNS at connection time, so the checked address is the connected one. */
|
|
58
56
|
export declare function createAssetCacheDispatcher(_required: Record<string, never>, { lookupImpl }?: {
|
|
59
57
|
lookupImpl?: DnsLookupCb;
|
|
60
58
|
}): Agent;
|
package/dist/asset-cache.js
CHANGED
|
@@ -130,21 +130,9 @@ export function createValidatingLookup(lookupImpl = dnsLookupCb) {
|
|
|
130
130
|
});
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
-
/** Default adapter: validate DNS at connection time, so the checked address is the connected one.
|
|
134
|
-
* @throws {AssetCacheError} When the host has not installed the optional undici peer.
|
|
135
|
-
*/
|
|
133
|
+
/** Default adapter: validate DNS at connection time, so the checked address is the connected one. */
|
|
136
134
|
export function createAssetCacheDispatcher(_required, { lookupImpl } = {}) {
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
return createUndiciAssetCacheDispatcher({ lookup: createValidatingLookup(lookupImpl) });
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
if (error instanceof Error && error.code === 'MODULE_NOT_FOUND'
|
|
143
|
-
&& error.message.split('\n')[0] === "Cannot find module 'undici'") {
|
|
144
|
-
throw new AssetCacheError(500, 'Asset cache dispatcher requires the optional peer "undici"; install undici@^7.25.0 or supply a guarded dispatcher.');
|
|
145
|
-
}
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
135
|
+
return createUndiciAssetCacheDispatcher({ lookup: createValidatingLookup(lookupImpl) });
|
|
148
136
|
}
|
|
149
137
|
/** Build a disk-backed cache for external sandboxed-content media. Concurrent
|
|
150
138
|
* requests for the same URL share one in-flight fetch; completed fetches
|
package/dist/blob-storage.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { promises as fsp } from 'node:fs';
|
|
22
|
+
import { trimChars, trimCharsEnd, trimCharsStart } from '@jini-ai/core/primitives';
|
|
22
23
|
import { encodeS3PathSegment, signSigV4 } from './aws-sigv4.js';
|
|
23
24
|
/** Error thrown by {@link BlobStorage} implementations, distinguishing not-found/traversal/IO failures. */
|
|
24
25
|
export class StorageError extends Error {
|
|
@@ -187,12 +188,13 @@ export class S3BlobStorage {
|
|
|
187
188
|
keyFor(namespace, relpath, { allowPrefix = false } = {}) {
|
|
188
189
|
validateNamespace(namespace);
|
|
189
190
|
const normalized = validateRelpath(relpath, { allowPrefix });
|
|
190
|
-
const
|
|
191
|
+
const prefix = this.options.prefix === undefined ? undefined : trimChars({ text: this.options.prefix, chars: '/' });
|
|
192
|
+
const segments = [prefix, namespace, normalized].filter((s) => typeof s === 'string' && s.length > 0);
|
|
191
193
|
return segments.join('/');
|
|
192
194
|
}
|
|
193
195
|
endpointBase() {
|
|
194
196
|
if (this.options.endpoint)
|
|
195
|
-
return this.options.endpoint
|
|
197
|
+
return trimCharsEnd({ text: this.options.endpoint, chars: '/' });
|
|
196
198
|
return `https://${this.options.bucket}.s3.${this.options.region}.amazonaws.com`;
|
|
197
199
|
}
|
|
198
200
|
async signedRequest(args) {
|
|
@@ -224,7 +226,8 @@ export class S3BlobStorage {
|
|
|
224
226
|
credentials: this.options.credentials,
|
|
225
227
|
now,
|
|
226
228
|
});
|
|
227
|
-
const
|
|
229
|
+
const query = args.extraQuery ? `?${args.extraQuery}` : '';
|
|
230
|
+
const url = `${trimCharsEnd({ text: base, chars: '/' })}${pathSegment}${query}`;
|
|
228
231
|
const init = {
|
|
229
232
|
method: args.method,
|
|
230
233
|
headers,
|
|
@@ -320,12 +323,10 @@ function namespaceEntries(entries, namespacePrefix) {
|
|
|
320
323
|
for (const e of entries) {
|
|
321
324
|
// It includes the non-empty, validated namespace and its trailing separator,
|
|
322
325
|
// so sibling namespaces (ns10 vs ns1) cannot leak into this listing.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
continue; // skip the prefix marker itself
|
|
328
|
-
out.push({ path: rel, size: e.size, mtimeMs: e.lastModifiedMs });
|
|
326
|
+
const rel = e.key.startsWith(namespacePrefix) ? trimCharsStart({ text: e.key.slice(namespacePrefix.length), chars: '/' }) : '';
|
|
327
|
+
// An empty rel is either a foreign key or the prefix marker itself; both are skipped.
|
|
328
|
+
if (rel)
|
|
329
|
+
out.push({ path: rel, size: e.size, mtimeMs: e.lastModifiedMs });
|
|
329
330
|
}
|
|
330
331
|
return out;
|
|
331
332
|
}
|
|
@@ -44,7 +44,7 @@ export function nodeLoginShellPorts() {
|
|
|
44
44
|
function parseNullSeparatedEnvironment({ stdout }) {
|
|
45
45
|
const entries = stdout.split('\0').flatMap(pair => {
|
|
46
46
|
const at = pair.indexOf('=');
|
|
47
|
-
return at > 0 && /^[A-Za-z_]
|
|
47
|
+
return at > 0 && /^[A-Za-z_]\w*$/.test(pair.slice(0, at)) ? [[pair.slice(0, at), pair.slice(at + 1)]] : [];
|
|
48
48
|
});
|
|
49
49
|
return Object.fromEntries(entries);
|
|
50
50
|
}
|
package/dist/command.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* itself.
|
|
12
12
|
*/
|
|
13
13
|
import { extname } from "node:path";
|
|
14
|
+
import { trimCharsEnd } from "@jini-ai/core/primitives";
|
|
14
15
|
// `cmd.exe /s /c "..."` runs percent-expansion on the inner line *regardless*
|
|
15
16
|
// of whether the `%name%` pair sits inside a `"..."` quoted segment, so a
|
|
16
17
|
// `.cmd` / `.bat` shim spawn with an attacker-influenced argv (e.g. an LLM
|
|
@@ -31,7 +32,9 @@ function quoteWindowsCommandArg({ value }, _options = {}) {
|
|
|
31
32
|
if (value !== "" && !/[\s"&<>|^%=,;()]/.test(value))
|
|
32
33
|
return value;
|
|
33
34
|
// Backslashes before the closing quote must survive the child's argv parser.
|
|
34
|
-
const
|
|
35
|
+
const quoted = value.replace(/"/g, '""').replace(/%/g, '"^%"');
|
|
36
|
+
const trailingBackslashes = quoted.slice(trimCharsEnd({ text: quoted, chars: '\\' }).length);
|
|
37
|
+
const escaped = quoted + trailingBackslashes;
|
|
35
38
|
return `"${escaped}"`;
|
|
36
39
|
}
|
|
37
40
|
// Build the `cmd.exe /d /s /c "<line>"` invocation Node uses internally for
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The port a URL actually connects to: its explicit port, else 443 for `https:` and 80 otherwise.
|
|
2
|
+
* @complexity O(1). */
|
|
3
|
+
export declare function effectivePort({ url }: {
|
|
4
|
+
url: URL;
|
|
5
|
+
}, _optional?: Record<string, never>): number;
|
|
6
|
+
//# sourceMappingURL=effective-port.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The port a URL actually connects to: its explicit port, else 443 for `https:` and 80 otherwise.
|
|
2
|
+
* @complexity O(1). */
|
|
3
|
+
export function effectivePort({ url }, _optional = {}) {
|
|
4
|
+
if (url.port)
|
|
5
|
+
return Number(url.port);
|
|
6
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=effective-port.js.map
|
package/dist/download.js
CHANGED
|
@@ -75,6 +75,7 @@ function normalizeChecksum(input) {
|
|
|
75
75
|
}
|
|
76
76
|
return { algorithm: input.algorithm, value };
|
|
77
77
|
}
|
|
78
|
+
const DOT_SEGMENTS = new Set([".", ".."]);
|
|
78
79
|
function normalizeSegment(value, label) {
|
|
79
80
|
if (typeof value !== "string" || value.length === 0) {
|
|
80
81
|
throw new ManagedDownloadError(MANAGED_DOWNLOAD_ERROR_CODES.INVALID_TARGET, `${label} must be a non-empty string`);
|
|
@@ -83,7 +84,7 @@ function normalizeSegment(value, label) {
|
|
|
83
84
|
// `isAbsolute`/`[\\/]` checks above don't reject it, but `path.win32.resolve`
|
|
84
85
|
// treats it as a real drive reference, which can escape `basePath` onto a
|
|
85
86
|
// different drive entirely.
|
|
86
|
-
if (
|
|
87
|
+
if (DOT_SEGMENTS.has(value) || value.includes("\0") || /[:\\/]/.test(value) || isAbsolute(value)) {
|
|
87
88
|
throw new ManagedDownloadError(MANAGED_DOWNLOAD_ERROR_CODES.INVALID_TARGET, `${label} must be a safe single path segment`);
|
|
88
89
|
}
|
|
89
90
|
return value;
|
|
@@ -210,15 +211,15 @@ async function ensureManagedBase(basePath) {
|
|
|
210
211
|
}
|
|
211
212
|
const sentinelPath = join(basePath, STORE_SENTINEL);
|
|
212
213
|
const sentinel = await readJson(sentinelPath);
|
|
214
|
+
if (sentinel != null && !isStoreSentinel(sentinel)) {
|
|
215
|
+
throw new ManagedDownloadError(MANAGED_DOWNLOAD_ERROR_CODES.STORE_NOT_OWNED, `download base has an invalid ownership marker: ${basePath}`);
|
|
216
|
+
}
|
|
213
217
|
if (sentinel == null) {
|
|
214
218
|
if (!(await directoryIsEmpty(basePath))) {
|
|
215
219
|
throw new ManagedDownloadError(MANAGED_DOWNLOAD_ERROR_CODES.STORE_NOT_OWNED, `download base is not empty and has no ownership marker: ${basePath}`);
|
|
216
220
|
}
|
|
217
221
|
await writeSentinel(basePath);
|
|
218
222
|
}
|
|
219
|
-
else if (!isStoreSentinel(sentinel)) {
|
|
220
|
-
throw new ManagedDownloadError(MANAGED_DOWNLOAD_ERROR_CODES.STORE_NOT_OWNED, `download base has an invalid ownership marker: ${basePath}`);
|
|
221
|
-
}
|
|
222
223
|
await ensureStoreDirs(basePath);
|
|
223
224
|
}
|
|
224
225
|
function isChecksum(value) {
|
|
@@ -241,21 +242,20 @@ function isManifest(value) {
|
|
|
241
242
|
if (!isPlainObject(value))
|
|
242
243
|
return false;
|
|
243
244
|
const record = value;
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
const versionMatches = record.kind === MANIFEST_KIND && record.schemaVersion === MANIFEST_SCHEMA_VERSION;
|
|
246
|
+
const knownState = record.state === "complete" || record.state === "partial";
|
|
247
|
+
return (versionMatches &&
|
|
246
248
|
MANIFEST_STRING_FIELDS.every((field) => typeof record[field] === "string") &&
|
|
247
249
|
isChecksum(record.checksum) &&
|
|
248
|
-
|
|
250
|
+
knownState);
|
|
249
251
|
}
|
|
250
252
|
function isDownloadLockFile(value) {
|
|
251
253
|
if (!isPlainObject(value))
|
|
252
254
|
return false;
|
|
253
255
|
const record = value;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
record.pid > 0 &&
|
|
258
|
-
(record.processStartedAt == null || typeof record.processStartedAt === "string");
|
|
256
|
+
const validPid = typeof record.pid === "number" && Number.isInteger(record.pid) && record.pid > 0;
|
|
257
|
+
const validStartedAt = record.processStartedAt == null || typeof record.processStartedAt === "string";
|
|
258
|
+
return typeof record.createdAt === "string" && validPid && validStartedAt;
|
|
259
259
|
}
|
|
260
260
|
function parseTimeMs(value) {
|
|
261
261
|
if (value == null)
|
|
@@ -293,14 +293,12 @@ async function readManifest(path) {
|
|
|
293
293
|
return "invalid";
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
+
const MANIFEST_IDENTITY_FIELDS = ["bucket", "fileName", "targetKey", "identityDigest", "urlDigest"];
|
|
296
297
|
function manifestMatchesTarget(manifest, target) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
manifest.
|
|
300
|
-
|
|
301
|
-
manifest.urlDigest === target.urlDigest &&
|
|
302
|
-
manifest.checksum.algorithm === target.checksum.algorithm &&
|
|
303
|
-
manifest.checksum.value.toLowerCase() === target.checksum.value);
|
|
298
|
+
const identityMatches = MANIFEST_IDENTITY_FIELDS.every((field) => manifest[field] === target[field]);
|
|
299
|
+
const checksumMatches = manifest.checksum.algorithm === target.checksum.algorithm &&
|
|
300
|
+
manifest.checksum.value.toLowerCase() === target.checksum.value;
|
|
301
|
+
return identityMatches && checksumMatches;
|
|
304
302
|
}
|
|
305
303
|
function createManifest(target, state, patch = {}) {
|
|
306
304
|
const now = new Date().toISOString();
|
|
@@ -468,10 +466,12 @@ async function downloadWithRetries(target, manifest, options) {
|
|
|
468
466
|
const deps = { emit: options.emit, fetchImpl: options.fetchImpl };
|
|
469
467
|
for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
|
|
470
468
|
try {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
469
|
+
const resume = nextManifest?.state === "partial"
|
|
470
|
+
? await tryResumeDownload({ ...deps, manifest: nextManifest, target }, options)
|
|
471
|
+
: null;
|
|
472
|
+
if (resume !== null && resume !== "restart")
|
|
473
|
+
return resume;
|
|
474
|
+
if (resume === "restart") {
|
|
475
475
|
await rm(target.partialPath, { force: true }).catch(() => undefined);
|
|
476
476
|
nextManifest = null;
|
|
477
477
|
}
|
|
@@ -859,9 +859,10 @@ async function removeEntriesOlderThan(root, olderThan) {
|
|
|
859
859
|
if (info == null || info.mtimeMs > olderThan)
|
|
860
860
|
continue;
|
|
861
861
|
const result = await removePathBestEffort(path);
|
|
862
|
+
// A successful removal never carries an error, so the two checks are exclusive.
|
|
862
863
|
if (result.removed)
|
|
863
864
|
removed += 1;
|
|
864
|
-
|
|
865
|
+
if (result.error != null)
|
|
865
866
|
warnings.push(result.error);
|
|
866
867
|
}
|
|
867
868
|
return { removed, warnings };
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* module has zero imports of its own — no `node:*` builtin, no `undici` — unlike the package's root
|
|
38
38
|
* barrel (`index.ts`), which re-exports sibling modules (`process.ts`, `fs.ts`, `terminal.ts`,
|
|
39
39
|
* `asset-cache.ts`, …) that import `node:child_process`, `node:fs`, etc.; the asset-cache
|
|
40
|
-
* dispatcher
|
|
40
|
+
* dispatcher constructs its required `undici` dependency only when its synchronous factory is called.
|
|
41
41
|
* Importing from the root barrel in browser-bundled code is silently fine in a *production* build
|
|
42
42
|
* through Vite/Rollup — tree-shaking (this package sets `sideEffects: false`) proves the Node-only
|
|
43
43
|
* re-exports are unused and drops them before the `node:*` imports are ever resolved — but it is
|
|
@@ -59,6 +59,15 @@ export declare class FetchTimeoutError extends Error {
|
|
|
59
59
|
readonly timeoutMs: number;
|
|
60
60
|
constructor(url: string, timeoutMs: number);
|
|
61
61
|
}
|
|
62
|
+
/** Observed response bytes exceeded the configured cap. Extends RangeError for existing catches;
|
|
63
|
+
* invalid cap configuration still throws a plain RangeError. */
|
|
64
|
+
export declare class ResponseLimitExceededError extends RangeError {
|
|
65
|
+
readonly limitBytes: number;
|
|
66
|
+
/** Records the byte ceiling without retaining response content. O(1) time and space. */
|
|
67
|
+
constructor({ limitBytes }: {
|
|
68
|
+
limitBytes: number;
|
|
69
|
+
}, _options?: Record<string, never>);
|
|
70
|
+
}
|
|
62
71
|
/**
|
|
63
72
|
* Shared, documented timeout classes for outbound `fetch()` calls. Every call site names one
|
|
64
73
|
* explicitly (or its own justified number) rather than relying on an unstated default.
|
|
@@ -96,22 +105,32 @@ export interface DeadlineSignalOptions {
|
|
|
96
105
|
startedAt?: number;
|
|
97
106
|
/** Clock port in milliseconds; defaults to Date.now and is read only with startedAt. */
|
|
98
107
|
now?: () => number;
|
|
108
|
+
/** Resolves after ms; aborting signal must release the wait and settle its promise. */
|
|
109
|
+
wait?: (input: {
|
|
110
|
+
ms: number;
|
|
111
|
+
signal: AbortSignal;
|
|
112
|
+
}) => Promise<void>;
|
|
99
113
|
}
|
|
100
114
|
/**
|
|
101
115
|
* Creates one signal that aborts when the remaining wait budget expires or the caller cancels.
|
|
102
116
|
* Spent time is subtracted once; future start times cannot increase the original budget.
|
|
103
117
|
* @param input - Wait budget in milliseconds. Negative remaining budgets become zero.
|
|
104
|
-
* @param options - Optional caller signal, start time and
|
|
105
|
-
* @returns A
|
|
118
|
+
* @param options - Optional caller signal, start time, clock and cancellable wait ports.
|
|
119
|
+
* @returns A signal preserving the first abort reason and an idempotent dispose function.
|
|
120
|
+
* Dispose in finally to cancel the wait and detach the parent without aborting the returned signal.
|
|
121
|
+
* Zero expires asynchronously,
|
|
106
122
|
* following AbortSignal.timeout(0); an already-aborted caller is reflected immediately.
|
|
107
|
-
*
|
|
123
|
+
* Uses real timers by default without mutating inputs or the caller's signal.
|
|
108
124
|
* @throws {RangeError} If the remaining delay is invalid for AbortSignal.timeout.
|
|
109
125
|
* @complexity O(1) time and space.
|
|
110
|
-
* @example createDeadlineSignal({ waitMs: 5_000 }, {
|
|
126
|
+
* @example const { signal, dispose } = createDeadlineSignal({ waitMs: 5_000 }, { wait });
|
|
111
127
|
*/
|
|
112
128
|
export declare function createDeadlineSignal({ waitMs }: {
|
|
113
129
|
waitMs: number;
|
|
114
|
-
}, { signal, startedAt, now }?: DeadlineSignalOptions):
|
|
130
|
+
}, { signal, startedAt, now, wait }?: DeadlineSignalOptions): {
|
|
131
|
+
signal: AbortSignal;
|
|
132
|
+
dispose: () => void;
|
|
133
|
+
};
|
|
115
134
|
/** Opt-in byte and decoding guards; defaults remain unlimited and replacement-decoded. */
|
|
116
135
|
export type TextReadOptions = {
|
|
117
136
|
/** Nonnegative safe integer. Actual streamed bytes, independent of Content-Length. */
|
|
@@ -124,7 +143,8 @@ export type TextReadOptions = {
|
|
|
124
143
|
* Details mode reports retained source bytes and truncates only on observed overflow;
|
|
125
144
|
* reaching the cap still requires EOF to establish completeness. Defaults reject overflow
|
|
126
145
|
* and replace malformed UTF-8. A supplied signal interrupts even a stalled read.
|
|
127
|
-
* @throws {RangeError} An invalid cap
|
|
146
|
+
* @throws {RangeError} An invalid cap.
|
|
147
|
+
* @throws {ResponseLimitExceededError} Observed bytes exceed the cap (also a RangeError).
|
|
128
148
|
* @throws {TypeError} Invalid UTF-8 when strict decoding is enabled.
|
|
129
149
|
* Read failures and abort reasons propagate; acquired readers are cancelled on failure
|
|
130
150
|
* and always released. Details mode observes cancellation failures without awaiting cleanup.
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* module has zero imports of its own — no `node:*` builtin, no `undici` — unlike the package's root
|
|
38
38
|
* barrel (`index.ts`), which re-exports sibling modules (`process.ts`, `fs.ts`, `terminal.ts`,
|
|
39
39
|
* `asset-cache.ts`, …) that import `node:child_process`, `node:fs`, etc.; the asset-cache
|
|
40
|
-
* dispatcher
|
|
40
|
+
* dispatcher constructs its required `undici` dependency only when its synchronous factory is called.
|
|
41
41
|
* Importing from the root barrel in browser-bundled code is silently fine in a *production* build
|
|
42
42
|
* through Vite/Rollup — tree-shaking (this package sets `sideEffects: false`) proves the Node-only
|
|
43
43
|
* re-exports are unused and drops them before the `node:*` imports are ever resolved — but it is
|
|
@@ -64,6 +64,17 @@ export class FetchTimeoutError extends Error {
|
|
|
64
64
|
this.timeoutMs = timeoutMs;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
/** Observed response bytes exceeded the configured cap. Extends RangeError for existing catches;
|
|
68
|
+
* invalid cap configuration still throws a plain RangeError. */
|
|
69
|
+
export class ResponseLimitExceededError extends RangeError {
|
|
70
|
+
limitBytes;
|
|
71
|
+
/** Records the byte ceiling without retaining response content. O(1) time and space. */
|
|
72
|
+
constructor({ limitBytes }, _options = {}) {
|
|
73
|
+
super(`response exceeds the ${limitBytes}-byte limit`);
|
|
74
|
+
this.name = "ResponseLimitExceededError";
|
|
75
|
+
this.limitBytes = limitBytes;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
67
78
|
/**
|
|
68
79
|
* Shared, documented timeout classes for outbound `fetch()` calls. Every call site names one
|
|
69
80
|
* explicitly (or its own justified number) rather than relying on an unstated default.
|
|
@@ -87,24 +98,53 @@ export const FETCH_TIMEOUT_MS = {
|
|
|
87
98
|
* legitimately still-generating request early. */
|
|
88
99
|
GENERATE: 10 * 60_000,
|
|
89
100
|
};
|
|
101
|
+
/** Browser-safe timer adapter; completion and cancellation both release its listener. */
|
|
102
|
+
function waitForDeadline({ ms, signal }, _options = {}) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
signal.throwIfAborted();
|
|
105
|
+
const abort = () => { clearTimeout(timer); reject(signal.reason); };
|
|
106
|
+
const timer = setTimeout(() => { signal.removeEventListener("abort", abort); resolve(); }, ms);
|
|
107
|
+
// Like native timeout signals, pending deadlines must not keep a Node process alive.
|
|
108
|
+
if (typeof timer === "object")
|
|
109
|
+
timer.unref();
|
|
110
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
90
113
|
/**
|
|
91
114
|
* Creates one signal that aborts when the remaining wait budget expires or the caller cancels.
|
|
92
115
|
* Spent time is subtracted once; future start times cannot increase the original budget.
|
|
93
116
|
* @param input - Wait budget in milliseconds. Negative remaining budgets become zero.
|
|
94
|
-
* @param options - Optional caller signal, start time and
|
|
95
|
-
* @returns A
|
|
117
|
+
* @param options - Optional caller signal, start time, clock and cancellable wait ports.
|
|
118
|
+
* @returns A signal preserving the first abort reason and an idempotent dispose function.
|
|
119
|
+
* Dispose in finally to cancel the wait and detach the parent without aborting the returned signal.
|
|
120
|
+
* Zero expires asynchronously,
|
|
96
121
|
* following AbortSignal.timeout(0); an already-aborted caller is reflected immediately.
|
|
97
|
-
*
|
|
122
|
+
* Uses real timers by default without mutating inputs or the caller's signal.
|
|
98
123
|
* @throws {RangeError} If the remaining delay is invalid for AbortSignal.timeout.
|
|
99
124
|
* @complexity O(1) time and space.
|
|
100
|
-
* @example createDeadlineSignal({ waitMs: 5_000 }, {
|
|
125
|
+
* @example const { signal, dispose } = createDeadlineSignal({ waitMs: 5_000 }, { wait });
|
|
101
126
|
*/
|
|
102
|
-
export function createDeadlineSignal({ waitMs }, { signal, startedAt, now = Date.now } = {}) {
|
|
127
|
+
export function createDeadlineSignal({ waitMs }, { signal, startedAt, now = Date.now, wait = waitForDeadline } = {}) {
|
|
103
128
|
const elapsedMs = startedAt === undefined ? 0 : Math.max(0, now() - startedAt);
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
const ms = Math.max(0, waitMs - elapsedMs);
|
|
130
|
+
if (!Number.isSafeInteger(ms))
|
|
131
|
+
throw new RangeError("remaining deadline must be a nonnegative safe integer");
|
|
132
|
+
const deadline = new AbortController();
|
|
133
|
+
const timer = new AbortController();
|
|
134
|
+
const dispose = () => { timer.abort(); signal?.removeEventListener("abort", onAbort); };
|
|
135
|
+
const finish = (reason) => {
|
|
136
|
+
if (!timer.signal.aborted)
|
|
137
|
+
deadline.abort(reason);
|
|
138
|
+
dispose();
|
|
139
|
+
};
|
|
140
|
+
const onAbort = () => finish(signal.reason);
|
|
141
|
+
if (signal?.aborted)
|
|
142
|
+
onAbort();
|
|
143
|
+
else {
|
|
144
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
145
|
+
void wait({ ms, signal: timer.signal }).then(() => finish(new DOMException("The operation was aborted due to timeout", "TimeoutError")), finish);
|
|
146
|
+
}
|
|
147
|
+
return { signal: deadline.signal, dispose };
|
|
108
148
|
}
|
|
109
149
|
export async function readFetchResponseText(input, { maxResponseBytes, strictUtf8 = false } = {}) {
|
|
110
150
|
const details = "result" in input && input.result === "details";
|
|
@@ -176,7 +216,7 @@ async function decodeResponseStream({ reader }, { maxResponseBytes = Infinity, s
|
|
|
176
216
|
const remaining = maxResponseBytes - total;
|
|
177
217
|
if (value.byteLength > remaining) {
|
|
178
218
|
if (onLimit === "reject")
|
|
179
|
-
throw new
|
|
219
|
+
throw new ResponseLimitExceededError({ limitBytes: maxResponseBytes });
|
|
180
220
|
text += decoder.decode(value.subarray(0, remaining), { stream: true });
|
|
181
221
|
return { text: text + decoder.decode(), byteLength: maxResponseBytes, truncated: true };
|
|
182
222
|
}
|
package/dist/fs/atomic-write.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as fsPromises from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { defaultPlatformMessages } from "../messages.js";
|
|
6
|
+
import { errorCodeOf } from "./errno.js";
|
|
6
7
|
/** Optional adapter wiring: no layout, env key, or application name is selected here. */
|
|
7
8
|
export function createNodeAtomicFilesystem(_required, { filesystem = fs } = {}) {
|
|
8
9
|
return {
|
|
@@ -19,7 +20,8 @@ export function createNodeAtomicFilesystem(_required, { filesystem = fs } = {})
|
|
|
19
20
|
}
|
|
20
21
|
const DIRECTORY_SYNC_UNSUPPORTED = ["EPERM", "EISDIR", "EINVAL", "EACCES", "ENOTSUP"];
|
|
21
22
|
function hasErrorCode(error, codes) {
|
|
22
|
-
|
|
23
|
+
const code = errorCodeOf(error);
|
|
24
|
+
return code !== undefined && codes.includes(code);
|
|
23
25
|
}
|
|
24
26
|
function isMissing(error) {
|
|
25
27
|
return hasErrorCode(error, ["ENOENT"]);
|
|
@@ -78,8 +80,9 @@ function checkedTempPath({ filePath, temporary }, options) {
|
|
|
78
80
|
throw new RangeError(messagesFor(options).atomicTempPathInvalid());
|
|
79
81
|
return temporary;
|
|
80
82
|
}
|
|
83
|
+
const DOT_SEGMENTS = new Set([".", ".."]);
|
|
81
84
|
function tokenTempPath({ filePath, token }, options) {
|
|
82
|
-
if (!token || token
|
|
85
|
+
if (!token || DOT_SEGMENTS.has(token) || token.includes("\0") || /[/\\]/.test(token))
|
|
83
86
|
throw new RangeError(messagesFor(options).atomicTempNameInvalid());
|
|
84
87
|
return path.join(path.dirname(filePath), `.${path.basename(filePath)}.${token}.tmp`);
|
|
85
88
|
}
|
|
@@ -14,10 +14,6 @@ export declare function readBootIdentity(_required: Record<string, never>, _opti
|
|
|
14
14
|
* beyond this tolerance proves an earlier boot; shorter gaps fall back to the PID probe.
|
|
15
15
|
*/
|
|
16
16
|
export declare const BOOT_CLOCK_TOLERANCE_MS: number;
|
|
17
|
-
/** True only when a recorded holder provably predates this boot. Linux boot_id is a random UUID and is
|
|
18
|
-
* compared exactly; numeric (time-derived) ids and legacy timestamps get the tolerance above. A record
|
|
19
|
-
* from a "later" boot is clock skew, never proof of death. @complexity O(1).
|
|
20
|
-
*/
|
|
21
17
|
export declare function fromEarlierBoot({ boot, bootId, atMs }: {
|
|
22
18
|
boot: BootIdentity | undefined;
|
|
23
19
|
bootId: string | undefined;
|
package/dist/fs/boot-identity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { windowsPowerShell } from "../windows-system-tools.js";
|
|
3
4
|
let cached;
|
|
4
5
|
const NATIVE_TIMEOUT_MS = 3000;
|
|
5
6
|
function linuxBootIdentity() {
|
|
@@ -16,8 +17,7 @@ function darwinBootIdentity() {
|
|
|
16
17
|
return timestampIdentity(Number(/sec\s*=\s*(\d+)/.exec(text)?.[1]) * 1000);
|
|
17
18
|
}
|
|
18
19
|
function windowsBootIdentity() {
|
|
19
|
-
|
|
20
|
-
const text = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o')"], { encoding: "utf8", timeout: NATIVE_TIMEOUT_MS, windowsHide: true });
|
|
20
|
+
const text = execFileSync(windowsPowerShell(), ["-NoProfile", "-NonInteractive", "-Command", "(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o')"], { encoding: "utf8", timeout: NATIVE_TIMEOUT_MS, windowsHide: true });
|
|
21
21
|
return timestampIdentity(Date.parse(text.trim()));
|
|
22
22
|
}
|
|
23
23
|
const PLATFORM_READERS = { linux: linuxBootIdentity, darwin: darwinBootIdentity, win32: windowsBootIdentity };
|
|
@@ -46,10 +46,16 @@ export const BOOT_CLOCK_TOLERANCE_MS = 10 * 60_000;
|
|
|
46
46
|
* compared exactly; numeric (time-derived) ids and legacy timestamps get the tolerance above. A record
|
|
47
47
|
* from a "later" boot is clock skew, never proof of death. @complexity O(1).
|
|
48
48
|
*/
|
|
49
|
+
/** Legacy records carry only a timestamp; time-derived boot ids are numeric; a UUID id has no time. */
|
|
50
|
+
function recordedBootTimeMs({ bootId, atMs }) {
|
|
51
|
+
if (bootId === undefined)
|
|
52
|
+
return atMs;
|
|
53
|
+
return /^\d+$/.test(bootId) ? Number(bootId) : undefined;
|
|
54
|
+
}
|
|
49
55
|
export function fromEarlierBoot({ boot, bootId, atMs }, _options = {}) {
|
|
50
56
|
if (!boot)
|
|
51
57
|
return false;
|
|
52
|
-
const recordedAtMs = bootId
|
|
58
|
+
const recordedAtMs = recordedBootTimeMs({ bootId, atMs });
|
|
53
59
|
return recordedAtMs === undefined ? bootId !== boot.id : recordedAtMs < boot.startedAtMs - BOOT_CLOCK_TOLERANCE_MS;
|
|
54
60
|
}
|
|
55
61
|
//# sourceMappingURL=boot-identity.js.map
|
|
@@ -14,8 +14,8 @@ export function createDirectoryRegistry(
|
|
|
14
14
|
if (result.state !== "ok")
|
|
15
15
|
throw new Error("The shared directory registry is unreadable; nothing was overwritten.");
|
|
16
16
|
const state = result.value;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const shapeOk = state?.version === 1 && Array.isArray(state.entries) && Array.isArray(state.migrations);
|
|
18
|
+
if (!shapeOk || !state.migrations.every((item) => typeof item === "string") || !state.entries.every(validRecord)) {
|
|
19
19
|
throw new Error("The shared directory registry has an invalid format; nothing was overwritten.");
|
|
20
20
|
}
|
|
21
21
|
return state;
|
|
@@ -67,11 +67,16 @@ function validRunningRecord(value) {
|
|
|
67
67
|
if (!value || typeof value !== "object")
|
|
68
68
|
return false;
|
|
69
69
|
const record = value;
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const validPid = Number.isSafeInteger(record.pid) && record.pid > 0;
|
|
71
|
+
const validPort = Number.isInteger(record.port) && record.port >= 0 && record.port <= 65535;
|
|
72
|
+
return validPid && validPort && validRunningIdentity({ record }) && ["http", "https"].includes(record.scheme);
|
|
72
73
|
}
|
|
73
74
|
function validRunningIdentity({ record }, _options = {}) {
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const validBootId = record.bootId === undefined || typeof record.bootId === "string";
|
|
76
|
+
const validStarted = typeof record.started === "string" && Number.isFinite(Date.parse(record.started));
|
|
77
|
+
return isNonEmptyString(record.app) && isNonEmptyString(record.token) && validBootId && validStarted;
|
|
78
|
+
}
|
|
79
|
+
function isNonEmptyString(value) {
|
|
80
|
+
return typeof value === "string" && value.length > 0;
|
|
76
81
|
}
|
|
77
82
|
//# sourceMappingURL=directory-running-lock.js.map
|