@jini-ai/platform 0.5.1 → 0.5.3

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 (74) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/asset-cache.d.ts +12 -1
  3. package/dist/asset-cache.js +9 -5
  4. package/dist/child-environment.d.ts +18 -0
  5. package/dist/child-environment.js +26 -0
  6. package/dist/fs/boot-identity.d.ts +10 -0
  7. package/dist/fs/boot-identity.js +34 -0
  8. package/dist/fs/directory-home.d.ts +15 -0
  9. package/dist/fs/directory-home.js +17 -0
  10. package/dist/fs/directory-registry.d.ts +50 -0
  11. package/dist/fs/directory-registry.js +99 -0
  12. package/dist/fs/directory-running-lock.d.ts +43 -0
  13. package/dist/fs/directory-running-lock.js +66 -0
  14. package/dist/fs/directory-sync.d.ts +20 -0
  15. package/dist/fs/directory-sync.js +87 -0
  16. package/dist/fs/directory-user-config.d.ts +79 -0
  17. package/dist/fs/directory-user-config.js +183 -0
  18. package/dist/fs/directory-workspace.d.ts +132 -0
  19. package/dist/fs/directory-workspace.js +266 -0
  20. package/dist/fs/durable-json.d.ts +21 -0
  21. package/dist/fs/durable-json.js +59 -4
  22. package/dist/fs/file-lock.d.ts +3 -0
  23. package/dist/fs/file-lock.js +16 -7
  24. package/dist/fs/index.d.ts +6 -0
  25. package/dist/fs/index.js +6 -0
  26. package/dist/fs/strict-containment.d.ts +1 -1
  27. package/dist/fs/strict-containment.js +1 -1
  28. package/dist/fs/user-data-directories.d.ts +28 -0
  29. package/dist/fs/user-data-directories.js +53 -0
  30. package/dist/fs/verified-directory-move.d.ts +37 -0
  31. package/dist/fs/verified-directory-move.js +138 -0
  32. package/dist/fs/verified-file-relocation.d.ts +27 -0
  33. package/dist/fs/verified-file-relocation.js +115 -0
  34. package/dist/fs/workspace-directory.d.ts +19 -0
  35. package/dist/fs/workspace-directory.js +23 -0
  36. package/dist/http/guarded/transport.fetch.d.ts +13 -1
  37. package/dist/http/guarded/transport.fetch.js +9 -3
  38. package/dist/index.d.ts +6 -54
  39. package/dist/index.js +6 -54
  40. package/dist/net/address.d.ts +1 -8
  41. package/dist/net/address.js +2 -89
  42. package/dist/net/endpoint-policy.d.ts +9 -0
  43. package/dist/net/endpoint-policy.js +98 -0
  44. package/dist/secrets/credential-sets/index.d.ts +5 -0
  45. package/dist/secrets/credential-sets/index.js +5 -0
  46. package/dist/secrets/credential-sets/repo.memory.d.ts +18 -10
  47. package/dist/secrets/credential-sets/repo.memory.js +18 -10
  48. package/dist/secrets/credential-sets/vault.aad.d.ts +4 -0
  49. package/dist/secrets/credential-sets/vault.aad.js +8 -0
  50. package/dist/secrets/credential-sets/vault.meta.d.ts +10 -0
  51. package/dist/secrets/credential-sets/vault.meta.js +52 -0
  52. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +51 -0
  53. package/dist/secrets/credential-sets/vault.repo.memory.js +148 -0
  54. package/dist/secrets/credential-sets/vault.service.d.ts +130 -0
  55. package/dist/secrets/credential-sets/vault.service.js +310 -0
  56. package/dist/secrets/credential-sets/vault.types.d.ts +150 -0
  57. package/dist/secrets/credential-sets/vault.types.js +28 -0
  58. package/dist/secrets/credential-token.d.ts +64 -0
  59. package/dist/secrets/credential-token.js +75 -0
  60. package/dist/secrets/index.d.ts +1 -0
  61. package/dist/secrets/index.js +1 -0
  62. package/dist/secrets/secret-sealer.aesgcm.d.ts +6 -0
  63. package/dist/secrets/secret-sealer.aesgcm.js +9 -2
  64. package/dist/web/html-document.d.ts +70 -0
  65. package/dist/web/html-document.js +268 -0
  66. package/dist/web/html-resource.d.ts +15 -0
  67. package/dist/web/html-resource.js +12 -0
  68. package/dist/web/html-to-markdown.d.ts +11 -0
  69. package/dist/web/html-to-markdown.js +245 -0
  70. package/dist/web/index.d.ts +5 -0
  71. package/dist/web/index.js +5 -0
  72. package/dist/web/web-page.d.ts +157 -0
  73. package/dist/web/web-page.js +310 -0
  74. package/package.json +26 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @jini-ai/platform
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Tovu clean-up release (2026-10-08): code Tovu moved into Jini, plus the Jini clean-up (see the commit log since 0.5.1).
8
+
3
9
  ## 0.5.1 — 2026-10-04
4
10
 
5
11
  - `./secrets` exports the canonical site-key names (`SiteKeyHandle`, `FixedSiteKeyKeyring`, `UnusableSiteKeyError`, `SiteKeyFileAlreadyExistsError`, `parseSiteKeyHex`, `fingerprintSiteKeyHex`, `inspectSiteKeyMaterial`, `revealSiteKeyMaterial`, `generateFileSiteKey`, `deriveFromSiteKey`, `deriveSigningSecretFromSiteKey` and the matching types). Every `RootKey*` name stays exported as a `@deprecated` alias of the same binding, to be removed on or after 2026-11-01. Additive: no behavior or signature change.
@@ -1,4 +1,5 @@
1
1
  import { lookup as dnsLookupCb } from 'node:dns';
2
+ import { Agent, type Dispatcher } from 'undici';
2
3
  /** Media extensions we are willing to cache + proxy. Anything else (HTML,
3
4
  * CSS, JS, fonts) is intentionally left alone so this surface never becomes a
4
5
  * general-purpose open proxy. */
@@ -43,15 +44,25 @@ export interface AssetCacheOptions {
43
44
  fetchImpl?: typeof fetch;
44
45
  /** Injectable `dns.lookup` for the connection-time SSRF guard (tests). */
45
46
  lookupImpl?: DnsLookupCb;
47
+ /** Host-supplied dispatcher must enforce the same connection-time public-address guard. */
48
+ dispatcher?: Dispatcher;
46
49
  }
47
50
  export interface AssetCache {
48
51
  get(rawUrl: string): Promise<AssetCacheResult>;
52
+ /** Closes only the default adapter; an injected dispatcher remains owned by its host. */
53
+ close?(): Promise<void>;
49
54
  }
55
+ /** Default adapter: validate DNS at connection time, so the checked address is the connected one. */
56
+ export declare function createAssetCacheDispatcher(_required: Record<string, never>, { lookupImpl }?: {
57
+ lookupImpl?: DnsLookupCb;
58
+ }): Agent;
50
59
  /** Build a disk-backed cache for external sandboxed-content media. Concurrent
51
60
  * requests for the same URL share one in-flight fetch; completed fetches
52
61
  * persist to `<cacheDir>/<sha256>` (+ `.json` sidecar) and are replayed from
53
62
  * disk. */
54
- export declare function createAssetCache(opts: AssetCacheOptions): AssetCache;
63
+ export declare function createAssetCache(opts: AssetCacheOptions, options?: {
64
+ dispatcher?: Dispatcher;
65
+ }): AssetCache;
55
66
  /** Exposed for tests that need to assert the on-disk key layout. */
56
67
  export declare function assetCacheKey(rawUrl: string): string;
57
68
  export {};
@@ -127,11 +127,15 @@ export function createValidatingLookup(lookupImpl = dnsLookupCb) {
127
127
  });
128
128
  };
129
129
  }
130
+ /** Default adapter: validate DNS at connection time, so the checked address is the connected one. */
131
+ export function createAssetCacheDispatcher(_required, { lookupImpl } = {}) {
132
+ return new Agent({ connect: { lookup: createValidatingLookup(lookupImpl) } });
133
+ }
130
134
  /** Build a disk-backed cache for external sandboxed-content media. Concurrent
131
135
  * requests for the same URL share one in-flight fetch; completed fetches
132
136
  * persist to `<cacheDir>/<sha256>` (+ `.json` sidecar) and are replayed from
133
137
  * disk. */
134
- export function createAssetCache(opts) {
138
+ export function createAssetCache(opts, options = {}) {
135
139
  const cacheDir = opts.cacheDir;
136
140
  const maxBytes = opts.maxBytes ?? 64 * 1024 * 1024;
137
141
  const timeoutMs = opts.fetchTimeoutMs ?? 15_000;
@@ -139,9 +143,8 @@ export function createAssetCache(opts) {
139
143
  // Pin SSRF validation to the actual outbound connection: the Agent resolves
140
144
  // the host once, and `validatingLookup` rejects the connection if that exact
141
145
  // address is private. No separate pre-validation lookup to rebind around.
142
- const dispatcher = new Agent({
143
- connect: { lookup: createValidatingLookup(opts.lookupImpl) },
144
- });
146
+ const suppliedDispatcher = options.dispatcher ?? opts.dispatcher;
147
+ const dispatcher = suppliedDispatcher ?? createAssetCacheDispatcher({}, opts.lookupImpl ? { lookupImpl: opts.lookupImpl } : {});
145
148
  const inflight = new Map();
146
149
  function keyFor(rawUrl) {
147
150
  return createHash('sha256').update(rawUrl).digest('hex');
@@ -279,7 +282,8 @@ export function createAssetCache(opts) {
279
282
  inflight.set(key, pending);
280
283
  return pending;
281
284
  }
282
- return { get };
285
+ return { get, close: async () => { if (!suppliedDispatcher)
286
+ await dispatcher.close(); } };
283
287
  }
284
288
  /** Exposed for tests that need to assert the on-disk key layout. */
285
289
  export function assetCacheKey(rawUrl) {
@@ -0,0 +1,18 @@
1
+ /** Environment isolation for child processes; hosts name the settings that belong to one run. */
2
+ /**
3
+ * Copy an environment without host-selected keys. Reports names only, never values; no I/O.
4
+ * Windows callers opt into case-insensitive matching so duplicate spellings cannot survive.
5
+ * @returns A fresh environment and the configured names whose defined values were omitted.
6
+ * @complexity O(e + k) time/space for e environment entries and k configured names.
7
+ * @example omitEnvironmentKeys({ env: parent, keys: ['WORKSPACE_STORAGE'] });
8
+ */
9
+ export declare function omitEnvironmentKeys({ env, keys }: {
10
+ env: Readonly<Record<string, string | undefined>>;
11
+ keys: readonly string[];
12
+ }, { caseInsensitive }?: {
13
+ caseInsensitive?: boolean;
14
+ }): {
15
+ env: Record<string, string | undefined>;
16
+ omitted: string[];
17
+ };
18
+ //# sourceMappingURL=child-environment.d.ts.map
@@ -0,0 +1,26 @@
1
+ /** Environment isolation for child processes; hosts name the settings that belong to one run. */
2
+ /**
3
+ * Copy an environment without host-selected keys. Reports names only, never values; no I/O.
4
+ * Windows callers opt into case-insensitive matching so duplicate spellings cannot survive.
5
+ * @returns A fresh environment and the configured names whose defined values were omitted.
6
+ * @complexity O(e + k) time/space for e environment entries and k configured names.
7
+ * @example omitEnvironmentKeys({ env: parent, keys: ['WORKSPACE_STORAGE'] });
8
+ */
9
+ export function omitEnvironmentKeys({ env, keys }, { caseInsensitive = false } = {}) {
10
+ const normalize = (key) => caseInsensitive ? key.toUpperCase() : key;
11
+ const names = new Map(keys.map((key) => [normalize(key), key]));
12
+ const child = {};
13
+ const omitted = new Set();
14
+ for (const [key, value] of Object.entries(env)) {
15
+ const name = names.get(normalize(key));
16
+ if (name !== undefined) {
17
+ if (value !== undefined)
18
+ omitted.add(name);
19
+ }
20
+ else {
21
+ child[key] = value;
22
+ }
23
+ }
24
+ return { env: child, omitted: [...omitted] };
25
+ }
26
+ //# sourceMappingURL=child-environment.js.map
@@ -0,0 +1,10 @@
1
+ export interface BootIdentity {
2
+ id: string;
3
+ startedAtMs: number;
4
+ }
5
+ /** Kernel boot identity distinguishes a recycled PID across reboots. Unknown identity fails safe
6
+ * to the existing liveness probe; no age timeout is used to evict a potentially live owner.
7
+ * Native calls are bounded and cached once per process. @complexity O(1) bounded OS reads.
8
+ */
9
+ export declare function readBootIdentity(_required: Record<string, never>, _options?: {}): BootIdentity | undefined;
10
+ //# sourceMappingURL=boot-identity.d.ts.map
@@ -0,0 +1,34 @@
1
+ import fs from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ let cached;
4
+ /** Kernel boot identity distinguishes a recycled PID across reboots. Unknown identity fails safe
5
+ * to the existing liveness probe; no age timeout is used to evict a potentially live owner.
6
+ * Native calls are bounded and cached once per process. @complexity O(1) bounded OS reads.
7
+ */
8
+ export function readBootIdentity(_required, _options = {}) {
9
+ if (cached !== undefined)
10
+ return cached ?? undefined;
11
+ cached = null;
12
+ try {
13
+ let startedAtMs;
14
+ if (process.platform === "linux") {
15
+ const id = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
16
+ startedAtMs = Number(/^btime (\d+)$/m.exec(fs.readFileSync("/proc/stat", "utf8"))?.[1]) * 1000;
17
+ if (id && Number.isFinite(startedAtMs))
18
+ cached = { id, startedAtMs };
19
+ }
20
+ else {
21
+ const text = process.platform === "darwin"
22
+ ? execFileSync("/usr/sbin/sysctl", ["-n", "kern.boottime"], { encoding: "utf8", timeout: 3000 })
23
+ : process.platform === "win32"
24
+ ? execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o')"], { encoding: "utf8", timeout: 3000, windowsHide: true })
25
+ : "";
26
+ startedAtMs = process.platform === "darwin" ? Number(/sec\s*=\s*(\d+)/.exec(text)?.[1]) * 1000 : Date.parse(text.trim());
27
+ if (Number.isFinite(startedAtMs))
28
+ cached = { id: String(startedAtMs), startedAtMs };
29
+ }
30
+ }
31
+ catch { /* Permission/OS failure is unknown identity, never proof of death. */ }
32
+ return cached ?? undefined;
33
+ }
34
+ //# sourceMappingURL=boot-identity.js.map
@@ -0,0 +1,15 @@
1
+ /** Resolve an operator-visible data home without I/O. Environment wins over settings.
2
+ * Empty overrides are errors; tilde expansion uses the supplied home, never a shell.
3
+ * @complexity O(n) in the chosen path length.
4
+ */
5
+ export declare function resolveDirectoryHome({ homeDir, defaultFolder, envName, env, setting }: {
6
+ homeDir: string;
7
+ defaultFolder: string;
8
+ envName: string;
9
+ env: Record<string, string | undefined>;
10
+ setting?: string | undefined;
11
+ }, { cwd, platform }?: {
12
+ cwd?: string;
13
+ platform?: NodeJS.Platform;
14
+ }): string;
15
+ //# sourceMappingURL=directory-home.d.ts.map
@@ -0,0 +1,17 @@
1
+ import path from "node:path";
2
+ /** Resolve an operator-visible data home without I/O. Environment wins over settings.
3
+ * Empty overrides are errors; tilde expansion uses the supplied home, never a shell.
4
+ * @complexity O(n) in the chosen path length.
5
+ */
6
+ export function resolveDirectoryHome({ homeDir, defaultFolder, envName, env, setting }, { cwd = homeDir, platform = process.platform } = {}) {
7
+ const paths = platform === "win32" ? path.win32 : path;
8
+ const selected = env[envName] ?? setting ?? paths.join(homeDir, defaultFolder);
9
+ if (!selected.trim())
10
+ throw new Error(`${envName} must name a directory`);
11
+ if (selected === "~")
12
+ return paths.resolve(homeDir);
13
+ if (selected.startsWith("~/") || selected.startsWith("~\\"))
14
+ return paths.resolve(homeDir, selected.slice(2));
15
+ return paths.resolve(cwd, selected);
16
+ }
17
+ //# sourceMappingURL=directory-home.js.map
@@ -0,0 +1,50 @@
1
+ import type { DurableJsonFile } from "./durable-json.js";
2
+ export interface DirectoryRecord {
3
+ id: string;
4
+ name: string;
5
+ path: string;
6
+ createdBy: string;
7
+ lastOpened: string | null;
8
+ }
9
+ export interface DirectoryRegistry {
10
+ hasMigration(required: {
11
+ migration: string;
12
+ }, options?: {}): boolean;
13
+ migrations(required: Record<string, never>, options?: {}): string[];
14
+ list(required: Record<string, never>, options?: {}): DirectoryRecord[];
15
+ merge(required: {
16
+ entries: readonly DirectoryRecord[];
17
+ migration?: string;
18
+ migrations?: readonly string[];
19
+ }, options?: {}): DirectoryRecord[];
20
+ opened(required: {
21
+ path: string;
22
+ at: string;
23
+ }, options?: {}): void;
24
+ remove(required: {
25
+ path: string;
26
+ }, options?: {}): void;
27
+ relocate(required: {
28
+ from: string;
29
+ to: string;
30
+ }, options?: {}): void;
31
+ }
32
+ /** Shared catalog: corrupt state fails closed; every update holds one durable file lock.
33
+ * Canonical paths deduplicate symlink aliases. Identity collisions require explicit relocation.
34
+ * Optional host admission prunes rejected entries on list under that same lock, then reports once.
35
+ * @complexity O(n + m) records for merge, O(n) for other mutations, plus file I/O/lock wait.
36
+ */
37
+ export declare function createDirectoryRegistry({ file, canonicalPath }: {
38
+ file: DurableJsonFile;
39
+ canonicalPath: (required: {
40
+ path: string;
41
+ }) => string;
42
+ }, { discard, reportDiscarded }?: {
43
+ discard?: (required: {
44
+ entry: DirectoryRecord;
45
+ }) => boolean;
46
+ reportDiscarded?: (required: {
47
+ entries: DirectoryRecord[];
48
+ }) => void;
49
+ }): DirectoryRegistry;
50
+ //# sourceMappingURL=directory-registry.d.ts.map
@@ -0,0 +1,99 @@
1
+ /** Shared catalog: corrupt state fails closed; every update holds one durable file lock.
2
+ * Canonical paths deduplicate symlink aliases. Identity collisions require explicit relocation.
3
+ * Optional host admission prunes rejected entries on list under that same lock, then reports once.
4
+ * @complexity O(n + m) records for merge, O(n) for other mutations, plus file I/O/lock wait.
5
+ */
6
+ export function createDirectoryRegistry({ file, canonicalPath }, { discard, reportDiscarded = () => { } } = {}) {
7
+ function read() {
8
+ const result = file.read({});
9
+ if (result.state === "missing")
10
+ return { version: 1, entries: [], migrations: [] };
11
+ if (result.state !== "ok")
12
+ throw new Error("The shared directory registry is unreadable; nothing was overwritten.");
13
+ const state = result.value;
14
+ if (state?.version !== 1 || !Array.isArray(state.entries) || !Array.isArray(state.migrations) ||
15
+ !state.migrations.every((item) => typeof item === "string") || !state.entries.every(validRecord)) {
16
+ throw new Error("The shared directory registry has an invalid format; nothing was overwritten.");
17
+ }
18
+ return state;
19
+ }
20
+ const canonical = (value) => canonicalPath({ path: value });
21
+ const update = (change) => file.withLock({ critical: () => {
22
+ const previous = read(), next = change(previous);
23
+ if (JSON.stringify(next) !== JSON.stringify(previous))
24
+ file.write({ value: next });
25
+ } });
26
+ const registry = {
27
+ hasMigration: ({ migration }, _optional = {}) => read().migrations.includes(migration),
28
+ migrations: (_required, _optional = {}) => read().migrations,
29
+ list: (_required, _optional = {}) => {
30
+ const current = read();
31
+ if (!discard || !current.entries.some((entry) => discard({ entry })))
32
+ return current.entries;
33
+ let entries = current.entries;
34
+ let removed = [];
35
+ // Re-read under the same mutation lock; never overwrite a sibling process's new rows.
36
+ update((state) => {
37
+ removed = state.entries.filter((entry) => discard({ entry }));
38
+ const discarded = new Set(removed);
39
+ entries = state.entries.filter((entry) => !discarded.has(entry));
40
+ return { ...state, entries };
41
+ });
42
+ if (removed.length)
43
+ reportDiscarded({ entries: removed });
44
+ return entries;
45
+ },
46
+ merge({ entries, migration, migrations = [] }, _optional = {}) {
47
+ let result = [];
48
+ update((state) => {
49
+ if (migration && state.migrations.includes(migration)) {
50
+ result = state.entries;
51
+ return state;
52
+ }
53
+ const byPath = new Map(state.entries.map((entry) => [canonical(entry.path), { ...entry, path: canonical(entry.path) }]));
54
+ const byId = new Map([...byPath.values()].map((entry) => [entry.id, entry.path]));
55
+ for (const entry of entries) {
56
+ if (!validRecord(entry))
57
+ throw new Error("Cannot register an invalid directory record.");
58
+ const dir = canonical(entry.path);
59
+ const previous = byPath.get(dir);
60
+ if (byId.has(entry.id) && byId.get(entry.id) !== dir)
61
+ throw new Error(`Directory identity ${entry.id} is already registered at another path.`);
62
+ // Reopening a site cannot reset who originally created it or its stable identity.
63
+ const lastOpened = [previous?.lastOpened, entry.lastOpened].filter((at) => typeof at === "string").sort((a, b) => Date.parse(a) - Date.parse(b)).at(-1) ?? null;
64
+ const next = previous ? { ...previous, name: entry.name, lastOpened } : { ...entry, path: dir };
65
+ byPath.set(dir, next);
66
+ byId.set(next.id, dir);
67
+ }
68
+ result = [...byPath.values()];
69
+ return { ...state, entries: result, migrations: [...new Set([...state.migrations, ...migrations, ...(migration ? [migration] : [])])] };
70
+ });
71
+ return result;
72
+ },
73
+ opened({ path, at }, _optional = {}) {
74
+ if (!Number.isFinite(Date.parse(at)))
75
+ throw new Error("The last-opened timestamp is invalid.");
76
+ update((state) => ({ ...state, entries: state.entries.map((entry) => canonical(entry.path) === canonical(path) ? { ...entry, lastOpened: at } : entry) }));
77
+ },
78
+ remove({ path }, _optional = {}) {
79
+ update((state) => ({ ...state, entries: state.entries.filter((entry) => canonical(entry.path) !== canonical(path)) }));
80
+ },
81
+ relocate({ from, to }, _optional = {}) {
82
+ update((state) => {
83
+ const target = canonical(to), source = canonical(from);
84
+ if (state.entries.some((entry) => canonical(entry.path) === target && canonical(entry.path) !== source))
85
+ throw new Error("Relocation would overwrite a registered directory.");
86
+ return { ...state, entries: state.entries.map((entry) => canonical(entry.path) === source ? { ...entry, path: target } : entry) };
87
+ });
88
+ },
89
+ };
90
+ return registry;
91
+ }
92
+ function validRecord(entry) {
93
+ if (!entry || typeof entry !== "object")
94
+ return false;
95
+ const row = entry;
96
+ return [row.id, row.name, row.path, row.createdBy].every((value) => typeof value === "string" && value.length > 0) &&
97
+ (row.lastOpened === null || (typeof row.lastOpened === "string" && Number.isFinite(Date.parse(row.lastOpened))));
98
+ }
99
+ //# sourceMappingURL=directory-registry.js.map
@@ -0,0 +1,43 @@
1
+ import type { BootIdentity } from "./boot-identity.js";
2
+ import type { DurableJsonFile } from "./durable-json.js";
3
+ export interface DirectoryRunningRecord {
4
+ pid: number;
5
+ port: number;
6
+ app: string;
7
+ started: string;
8
+ token: string;
9
+ scheme: "http" | "https";
10
+ bootId?: string;
11
+ }
12
+ export interface DirectoryRunningLock {
13
+ peek(required: Record<string, never>, options?: {}): DirectoryRunningRecord | null;
14
+ read(required: Record<string, never>, options?: {}): DirectoryRunningRecord | null;
15
+ claim(required: {
16
+ record: DirectoryRunningRecord;
17
+ }, options?: {}): {
18
+ acquired: boolean;
19
+ record: DirectoryRunningRecord;
20
+ };
21
+ update(required: {
22
+ token: string;
23
+ port: number;
24
+ }, options?: {}): void;
25
+ release(required: {
26
+ token: string;
27
+ }, options?: {}): void;
28
+ }
29
+ /** A process lease is distinct from the brief file lock protecting its read/modify/write.
30
+ * Only proven process death permits stale cleanup. Release/update require the unique claim token.
31
+ * A zero port means boot is in progress and must still prevent a second database owner.
32
+ * @complexity O(1) bounded JSON I/O and one liveness probe, plus the file lock wait.
33
+ */
34
+ export declare function createDirectoryRunningLock({ file, isAlive, remove }: {
35
+ file: DurableJsonFile;
36
+ isAlive: (required: {
37
+ pid: number;
38
+ }) => boolean;
39
+ remove: () => void;
40
+ }, { bootIdentity }?: {
41
+ bootIdentity?: () => BootIdentity | undefined;
42
+ }): DirectoryRunningLock;
43
+ //# sourceMappingURL=directory-running-lock.d.ts.map
@@ -0,0 +1,66 @@
1
+ /** A process lease is distinct from the brief file lock protecting its read/modify/write.
2
+ * Only proven process death permits stale cleanup. Release/update require the unique claim token.
3
+ * A zero port means boot is in progress and must still prevent a second database owner.
4
+ * @complexity O(1) bounded JSON I/O and one liveness probe, plus the file lock wait.
5
+ */
6
+ export function createDirectoryRunningLock({ file, isAlive, remove }, { bootIdentity } = {}) {
7
+ function readLive(cleanup = true) {
8
+ const result = file.read({});
9
+ if (result.state === "missing")
10
+ return null;
11
+ if (result.state !== "ok")
12
+ throw new Error("The running lock is unreadable; refusing to open this directory's database.");
13
+ const record = result.value;
14
+ if (!validRunningRecord(record))
15
+ throw new Error("The running lock is invalid; refusing to open this directory's database.");
16
+ const boot = bootIdentity?.();
17
+ const sameBoot = !boot || (record.bootId !== undefined ? record.bootId === boot.id : Date.parse(record.started) >= boot.startedAtMs);
18
+ if (sameBoot && isAlive({ pid: record.pid }))
19
+ return record;
20
+ if (cleanup)
21
+ remove();
22
+ return null;
23
+ }
24
+ return {
25
+ peek: (_required, _optional = {}) => readLive(false),
26
+ read: (_required, _optional = {}) => file.read({}).state === "missing" ? null : file.withLock({ critical: () => readLive() }),
27
+ claim({ record }, _optional = {}) {
28
+ if (!validRunningRecord(record))
29
+ throw new Error("Cannot claim an invalid running lock.");
30
+ return file.withLock({ critical: () => {
31
+ const existing = readLive();
32
+ if (existing)
33
+ return { acquired: false, record: existing };
34
+ const boot = bootIdentity?.();
35
+ const claimed = boot ? { ...record, bootId: boot.id } : record;
36
+ file.write({ value: claimed });
37
+ return { acquired: true, record: claimed };
38
+ } });
39
+ },
40
+ update({ token, port }, _optional = {}) {
41
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
42
+ throw new Error("Running port must be between 1 and 65535.");
43
+ file.withLock({ critical: () => {
44
+ const record = readLive();
45
+ if (!record || record.token !== token)
46
+ throw new Error("Running lock ownership was lost.");
47
+ file.write({ value: { ...record, port } });
48
+ } });
49
+ },
50
+ release({ token }, _optional = {}) {
51
+ if (file.read({}).state === "missing")
52
+ return;
53
+ file.withLock({ critical: () => { if (readLive()?.token === token)
54
+ remove(); } });
55
+ },
56
+ };
57
+ }
58
+ function validRunningRecord(value) {
59
+ if (!value || typeof value !== "object")
60
+ return false;
61
+ const record = value;
62
+ return Number.isSafeInteger(record.pid) && record.pid > 0 && Number.isInteger(record.port) && record.port >= 0 && record.port <= 65535 &&
63
+ typeof record.app === "string" && record.app.length > 0 && typeof record.token === "string" && record.token.length > 0 &&
64
+ (record.bootId === undefined || typeof record.bootId === "string") && typeof record.started === "string" && Number.isFinite(Date.parse(record.started)) && ["http", "https"].includes(record.scheme);
65
+ }
66
+ //# sourceMappingURL=directory-running-lock.js.map
@@ -0,0 +1,20 @@
1
+ import { type UserDirectoryOptions } from "./user-data-directories.js";
2
+ export type DirectorySyncProvider = "iCloud" | "OneDrive" | "Dropbox" | "Google Drive";
3
+ /** Detect provider roots, never infer syncing just because a provider is installed. Canonical
4
+ * containment catches redirected Documents, while marker files cover older client layouts.
5
+ * Dropbox publishes info.json paths; macOS File Provider mounts identify their provider.
6
+ * @complexity O(a + p) small account/mount lists and ancestor probes, no recursive site scan.
7
+ */
8
+ export declare function detectDirectorySync({ dir, homeDir, documents }: {
9
+ dir: string;
10
+ homeDir: string;
11
+ documents: string;
12
+ }, { env, platform, readText, canonicalPath, listDirectories, exists, oneDriveRoots, windowsOneDriveRoots, }?: UserDirectoryOptions & {
13
+ canonicalPath?: (required: {
14
+ path: string;
15
+ }) => string;
16
+ listDirectories?: (root: string) => string[];
17
+ exists?: (file: string) => boolean;
18
+ oneDriveRoots?: readonly string[];
19
+ }): DirectorySyncProvider | null;
20
+ //# sourceMappingURL=directory-sync.d.ts.map
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readOptionalText, readWindowsOneDriveRoots } from "./user-data-directories.js";
4
+ /** Detect provider roots, never infer syncing just because a provider is installed. Canonical
5
+ * containment catches redirected Documents, while marker files cover older client layouts.
6
+ * Dropbox publishes info.json paths; macOS File Provider mounts identify their provider.
7
+ * @complexity O(a + p) small account/mount lists and ancestor probes, no recursive site scan.
8
+ */
9
+ export function detectDirectorySync({ dir, homeDir, documents }, { env = process.env, platform = process.platform, readText = (file) => readOptionalText({ file }), canonicalPath = ({ path: value }) => { try {
10
+ return fs.realpathSync(value);
11
+ }
12
+ catch {
13
+ return path.resolve(value);
14
+ } }, listDirectories = (root) => { try {
15
+ return fs.readdirSync(root);
16
+ }
17
+ catch (error) {
18
+ if (error.code === "ENOENT")
19
+ return [];
20
+ throw error;
21
+ } }, exists = fs.existsSync, oneDriveRoots = [], windowsOneDriveRoots = () => readWindowsOneDriveRoots({}), } = {}) {
22
+ const paths = platform === "win32" ? path.win32 : path.posix;
23
+ const roots = [];
24
+ const add = (root, provider) => { if (root)
25
+ roots.push({ root, provider }); };
26
+ if (platform === "darwin") {
27
+ const cloud = paths.join(homeDir, "Library", "Mobile Documents", "com~apple~CloudDocs");
28
+ add(cloud, "iCloud");
29
+ // Documents need not be a symlink on newer macOS: the cloud Documents entry is evidence
30
+ // of Desktop & Documents being enabled, rather than merely iCloud Drive being installed.
31
+ if (exists(paths.join(cloud, "Documents")))
32
+ add(documents, "iCloud");
33
+ const mounts = paths.join(homeDir, "Library", "CloudStorage");
34
+ for (const name of listDirectories(mounts)) {
35
+ if (name.startsWith("OneDrive-"))
36
+ add(paths.join(mounts, name), "OneDrive");
37
+ else if (name.startsWith("GoogleDrive-"))
38
+ add(paths.join(mounts, name), "Google Drive");
39
+ else if (name.startsWith("Dropbox"))
40
+ add(paths.join(mounts, name), "Dropbox");
41
+ }
42
+ }
43
+ for (const root of [env.OneDrive, env.OneDriveConsumer, env.OneDriveCommercial, ...oneDriveRoots])
44
+ add(root, "OneDrive");
45
+ if (platform === "win32" && !env.OneDrive && !env.OneDriveConsumer && !env.OneDriveCommercial) {
46
+ for (const root of windowsOneDriveRoots())
47
+ add(root, "OneDrive");
48
+ }
49
+ const dropboxFiles = platform === "win32" ? [env.APPDATA, env.LOCALAPPDATA].filter((root) => Boolean(root)).map((root) => paths.join(root, "Dropbox", "info.json")) : [paths.join(homeDir, ".dropbox", "info.json")];
50
+ for (const file of dropboxFiles) {
51
+ const text = readText(file);
52
+ if (!text)
53
+ continue;
54
+ // Provider-owned metadata can be torn/corrupt; it is optional evidence, never a boot gate.
55
+ let accounts;
56
+ try {
57
+ accounts = JSON.parse(text);
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ if (!accounts || typeof accounts !== "object" || Array.isArray(accounts))
63
+ continue;
64
+ for (const account of Object.values(accounts))
65
+ if (typeof account?.path === "string")
66
+ add(account.path, "Dropbox");
67
+ }
68
+ const canonical = (value) => { const resolved = canonicalPath({ path: value }); return platform === "win32" ? resolved.toLowerCase() : resolved; };
69
+ const target = canonical(dir);
70
+ for (const { root, provider } of roots) {
71
+ const resolved = canonical(root);
72
+ if (target === resolved || target.startsWith(resolved + paths.sep))
73
+ return provider;
74
+ }
75
+ let ancestor = paths.resolve(dir);
76
+ while (true) {
77
+ if (exists(paths.join(ancestor, ".dropbox.cache")))
78
+ return "Dropbox";
79
+ if (exists(paths.join(ancestor, ".tmp.drivedownload")) || exists(paths.join(ancestor, ".tmp.driveupload")))
80
+ return "Google Drive";
81
+ const parent = paths.dirname(ancestor);
82
+ if (parent === ancestor)
83
+ return null;
84
+ ancestor = parent;
85
+ }
86
+ }
87
+ //# sourceMappingURL=directory-sync.js.map