@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
@@ -0,0 +1,138 @@
1
+ import { stageVerifiedFileRelocation } from "./verified-file-relocation.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ /** Fingerprint relative names, entry kinds, symlink targets and file bytes without following links.
6
+ * This is verification, never a log of file contents. Lease files are excluded by the host.
7
+ * @complexity O(bytes + entries log entries), O(entries + a fixed 64 KiB read buffer) space.
8
+ */
9
+ export function directoryFingerprint({ dir }, { excluded = [], filesystem = fs } = {}) {
10
+ const digest = createHash("sha256");
11
+ function visit(relative) {
12
+ for (const entry of filesystem.readdirSync(path.join(dir, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
13
+ if (!relative && excluded.includes(entry.name))
14
+ continue;
15
+ const name = path.join(relative, entry.name), absolute = path.join(dir, name);
16
+ const stat = filesystem.lstatSync(absolute);
17
+ const kind = stat.isSymbolicLink() ? "link" : stat.isDirectory() ? "directory" : stat.isFile() ? "file" : "unsupported";
18
+ if (kind === "unsupported")
19
+ throw new Error(`Cannot move special filesystem entry: ${name}`);
20
+ digest.update(JSON.stringify([name, kind, stat.mode & 0o777]));
21
+ if (kind === "directory")
22
+ visit(name);
23
+ else if (kind === "link")
24
+ digest.update(JSON.stringify(filesystem.readlinkSync(absolute)));
25
+ else {
26
+ const fileDigest = createHash("sha256"), buffer = Buffer.allocUnsafe(64 * 1024);
27
+ const descriptor = filesystem.openSync(absolute, "r");
28
+ try {
29
+ let bytes;
30
+ while ((bytes = filesystem.readSync(descriptor, buffer, 0, buffer.length, null)) > 0)
31
+ fileDigest.update(buffer.subarray(0, bytes));
32
+ }
33
+ finally {
34
+ filesystem.closeSync(descriptor);
35
+ }
36
+ digest.update(fileDigest.digest());
37
+ }
38
+ }
39
+ }
40
+ visit("");
41
+ return digest.digest("hex");
42
+ }
43
+ /** Move only into an absent target. Verify before registry commit or source deletion.
44
+ * EXDEV uses copy/verify/retire/commit; failure restores the source and removes only our new copy.
45
+ * Same-filesystem failures roll the rename back. Commit must atomically update the host registry.
46
+ * @complexity O(bytes + entries log entries), with two verification passes.
47
+ */
48
+ export function moveDirectoryVerified({ from, to, commit }, { excluded = [], filesystem = fs, prepareDestination, relocateFiles, verifyFile, backupId = randomUUID } = {}) {
49
+ const source = filesystem.realpathSync(from);
50
+ const target = path.resolve(to);
51
+ if (target === source || target.startsWith(source + path.sep))
52
+ throw new Error("A directory cannot be moved into itself.");
53
+ try {
54
+ filesystem.lstatSync(target);
55
+ throw new Error(`Move target already exists: ${target}`);
56
+ }
57
+ catch (error) {
58
+ if (error.code !== "ENOENT")
59
+ throw error;
60
+ }
61
+ const before = directoryFingerprint({ dir: source }, { excluded, filesystem });
62
+ let copied = false;
63
+ try {
64
+ filesystem.renameSync(source, target);
65
+ }
66
+ catch (error) {
67
+ if (error.code !== "EXDEV")
68
+ throw error;
69
+ filesystem.mkdirSync(target);
70
+ copied = true;
71
+ try {
72
+ // The host may reserve the empty destination before any commit marker or database is copied.
73
+ prepareDestination?.({ path: target });
74
+ filesystem.cpSync(source, target, { recursive: true, force: false, errorOnExist: true, preserveTimestamps: true, verbatimSymlinks: true,
75
+ filter: (candidate) => candidate === source || !excluded.includes(path.relative(source, candidate).split(path.sep)[0]),
76
+ });
77
+ }
78
+ catch (failure) {
79
+ filesystem.rmSync(target, { recursive: true });
80
+ throw failure;
81
+ }
82
+ }
83
+ let retiredSource;
84
+ let staged;
85
+ try {
86
+ if (directoryFingerprint({ dir: target }, { excluded, filesystem }) !== before)
87
+ throw new Error("Moved directory verification failed.");
88
+ if (relocateFiles) {
89
+ if (!verifyFile)
90
+ throw new Error("File relocation requires a verification port.");
91
+ staged = stageVerifiedFileRelocation({ groups: relocateFiles({ source, target }), verify: verifyFile }, { filesystem, backupId, backupCovered: ({ from }) => copied && path.resolve(from).startsWith(target + path.sep) });
92
+ }
93
+ if (copied) {
94
+ const backupRoot = path.join(path.dirname(source), `.relocation-${backupId()}-backup`);
95
+ const backup = path.join(backupRoot, path.basename(source));
96
+ if (filesystem.existsSync(backup))
97
+ throw new Error(`Move backup target already exists: ${backup}`);
98
+ // The EXDEV original becomes a recovery folder before catalog commit, never a second live
99
+ // site at the old path if cleanup fails. An interrupted apply leaves explicit backups.
100
+ filesystem.mkdirSync(backupRoot);
101
+ filesystem.renameSync(source, backup);
102
+ retiredSource = backup;
103
+ const fd = filesystem.openSync(backupRoot, "r");
104
+ try {
105
+ filesystem.fsyncSync(fd);
106
+ }
107
+ finally {
108
+ filesystem.closeSync(fd);
109
+ }
110
+ }
111
+ // Persist directory-name changes for same-filesystem renames as well as EXDEV retirement.
112
+ for (const directory of new Set([path.dirname(source), path.dirname(target)])) {
113
+ const fd = filesystem.openSync(directory, "r");
114
+ try {
115
+ filesystem.fsyncSync(fd);
116
+ }
117
+ finally {
118
+ filesystem.closeSync(fd);
119
+ }
120
+ }
121
+ commit();
122
+ }
123
+ catch (error) {
124
+ if (retiredSource)
125
+ filesystem.renameSync(retiredSource, source);
126
+ staged?.rollback();
127
+ if (copied)
128
+ filesystem.rmSync(target, { recursive: true });
129
+ else
130
+ filesystem.renameSync(target, source);
131
+ throw error;
132
+ }
133
+ // If deletion fails after commit, the verified target and registry remain usable; old DB live
134
+ // names were already retired before commit. Never roll back a registry whose commit succeeded.
135
+ staged?.finalize();
136
+ // Keep the retired EXDEV recovery folder; it is never a registered/live site path.
137
+ }
138
+ //# sourceMappingURL=verified-directory-move.js.map
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs";
2
+ export interface VerifiedFileGroup {
3
+ from: string;
4
+ to: string;
5
+ sidecars: readonly string[];
6
+ }
7
+ /** Stage related files together while both directory identities are reserved by the host. Raw
8
+ * sources remain intact through inspection so even WAL/SHM bytes are retained. Verification opens only
9
+ * stopped databases through an injected read-only inspector; the filesystem owner never uses SQL.
10
+ * @complexity O(total bytes + verification cost), bounded memory via native file copies.
11
+ */
12
+ export declare function stageVerifiedFileRelocation({ groups, verify }: {
13
+ groups: readonly VerifiedFileGroup[];
14
+ verify: (required: {
15
+ path: string;
16
+ }) => string;
17
+ }, { filesystem, backupCovered, backupId }?: {
18
+ filesystem?: typeof fs;
19
+ backupCovered?: (required: {
20
+ from: string;
21
+ }) => boolean;
22
+ backupId?: () => string;
23
+ }): {
24
+ rollback: () => void;
25
+ finalize: () => void;
26
+ };
27
+ //# sourceMappingURL=verified-file-relocation.d.ts.map
@@ -0,0 +1,115 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ /** Stage related files together while both directory identities are reserved by the host. Raw
5
+ * sources remain intact through inspection so even WAL/SHM bytes are retained. Verification opens only
6
+ * stopped databases through an injected read-only inspector; the filesystem owner never uses SQL.
7
+ * @complexity O(total bytes + verification cost), bounded memory via native file copies.
8
+ */
9
+ export function stageVerifiedFileRelocation({ groups, verify }, { filesystem = fs, backupCovered = () => false, backupId = randomUUID } = {}) {
10
+ const created = [], retired = [], markers = [], redundantBackups = [];
11
+ const backupSuffix = `.relocation-${backupId()}-backup`;
12
+ const copyExclusive = (from, to) => {
13
+ // Reserve before copying so a disk-full failure is still ours to clean up. COPYFILE_EXCL
14
+ // alone may create a partial file before throwing, which cannot be tracked after the fact.
15
+ const fd = filesystem.openSync(to, "wx");
16
+ created.push(to);
17
+ filesystem.closeSync(fd);
18
+ filesystem.copyFileSync(from, to);
19
+ const copied = filesystem.openSync(to, "r+");
20
+ try {
21
+ filesystem.fsyncSync(copied);
22
+ }
23
+ finally {
24
+ filesystem.closeSync(copied);
25
+ }
26
+ syncParent(to);
27
+ };
28
+ const syncParent = (file) => {
29
+ // Directory fsync persists retirement/marker ordering before the host commits its catalog.
30
+ const fd = filesystem.openSync(path.dirname(file), "r");
31
+ try {
32
+ filesystem.fsyncSync(fd);
33
+ }
34
+ finally {
35
+ filesystem.closeSync(fd);
36
+ }
37
+ };
38
+ const rollback = () => {
39
+ for (const item of [...retired].reverse()) {
40
+ filesystem.renameSync(item.backup, item.from);
41
+ syncParent(item.from);
42
+ }
43
+ for (const file of [...created].reverse()) {
44
+ filesystem.rmSync(file, { force: true });
45
+ syncParent(file);
46
+ }
47
+ };
48
+ try {
49
+ for (const group of groups) {
50
+ const pairs = ["", ...group.sidecars].filter((suffix) => filesystem.existsSync(group.from + suffix))
51
+ .map((suffix) => ({ from: group.from + suffix, to: group.to + suffix }));
52
+ for (const pair of pairs)
53
+ if (!filesystem.lstatSync(pair.from).isFile())
54
+ throw new Error(`Database move requires a regular file: ${pair.from}`);
55
+ filesystem.mkdirSync(path.dirname(group.to), { recursive: true });
56
+ const moving = path.resolve(group.from) !== path.resolve(group.to);
57
+ // A directory rename already carries unchanged DB paths atomically; verify those bytes
58
+ // without copying them. For a file move the original inode becomes the sole backup after
59
+ // verification: another full destination backup doubles storage without rollback benefit.
60
+ // Cross-volume directory moves already retain the untouched original directory. A copied
61
+ // DB inside that destination needs no second full backup of the same bytes.
62
+ if (moving) {
63
+ const marker = group.from + ".home2-relocation.json";
64
+ const fd = filesystem.openSync(marker, "wx");
65
+ created.push(marker);
66
+ markers.push(marker);
67
+ try {
68
+ filesystem.writeFileSync(fd, JSON.stringify({ to: group.to, backup: group.from + backupSuffix }));
69
+ filesystem.fsyncSync(fd);
70
+ }
71
+ finally {
72
+ filesystem.closeSync(fd);
73
+ }
74
+ syncParent(marker);
75
+ }
76
+ const before = verify({ path: group.from });
77
+ for (const pair of pairs) {
78
+ if (path.resolve(pair.from) === path.resolve(pair.to))
79
+ continue;
80
+ // A destination collision aborts; never overwrite another site's data or an old move.
81
+ copyExclusive(pair.from, pair.to);
82
+ }
83
+ if (verify({ path: group.to }) !== before)
84
+ throw new Error(`Database row counts changed while moving ${path.basename(group.to)}.`);
85
+ // Retire old live names BEFORE commit. Finalization removes recovery markers and redundant copies covered by the retained original: a crash
86
+ // or cleanup failure cannot leave two independently writable database names after commit.
87
+ if (moving)
88
+ for (const pair of pairs) {
89
+ const backup = pair.from + backupSuffix;
90
+ if (filesystem.existsSync(backup))
91
+ throw new Error(`Database backup target already exists: ${backup}`);
92
+ filesystem.renameSync(pair.from, backup);
93
+ retired.push({ from: pair.from, backup });
94
+ if (backupCovered({ from: pair.from }))
95
+ redundantBackups.push(backup);
96
+ syncParent(pair.from);
97
+ }
98
+ }
99
+ }
100
+ catch (error) {
101
+ rollback();
102
+ throw error;
103
+ }
104
+ return { rollback, finalize: () => {
105
+ for (const backup of redundantBackups) {
106
+ filesystem.rmSync(backup);
107
+ syncParent(backup);
108
+ }
109
+ for (const marker of markers) {
110
+ filesystem.rmSync(marker);
111
+ syncParent(marker);
112
+ }
113
+ } };
114
+ }
115
+ //# sourceMappingURL=verified-file-relocation.js.map
@@ -0,0 +1,19 @@
1
+ import type { DirectoryRecord } from "./directory-registry.js";
2
+ /** Shared selection policy for CLI/dev hosts. A registry ID disambiguates adopted paths;
3
+ * a basename selects the home first. No initialized home site preserves the supplied fallback.
4
+ * @complexity O(n) registered/child directories and validation probes.
5
+ */
6
+ export declare function selectWorkspaceDirectory({ home, entries, fallback, selected, roots, listDirectories, isInitialized }: {
7
+ home: string;
8
+ entries: readonly DirectoryRecord[];
9
+ fallback: string;
10
+ selected?: string | undefined;
11
+ roots?: readonly string[];
12
+ listDirectories: (required: {
13
+ root: string;
14
+ }) => readonly string[];
15
+ isInitialized: (required: {
16
+ path: string;
17
+ }) => boolean;
18
+ }, _options?: {}): string;
19
+ //# sourceMappingURL=workspace-directory.d.ts.map
@@ -0,0 +1,23 @@
1
+ import path from "node:path";
2
+ /** Shared selection policy for CLI/dev hosts. A registry ID disambiguates adopted paths;
3
+ * a basename selects the home first. No initialized home site preserves the supplied fallback.
4
+ * @complexity O(n) registered/child directories and validation probes.
5
+ */
6
+ export function selectWorkspaceDirectory({ home, entries, fallback, selected, roots = [home], listDirectories, isInitialized }, _options = {}) {
7
+ if (selected !== undefined) {
8
+ const byId = entries.find((entry) => entry.id === selected);
9
+ if (byId)
10
+ return byId.path;
11
+ const inHome = path.join(home, selected);
12
+ if (isInitialized({ path: inHome }))
13
+ return inHome;
14
+ const matches = [...new Set([...entries.filter((entry) => path.basename(entry.path) === selected).map((entry) => entry.path),
15
+ ...roots.filter((root) => root !== home).map((root) => path.join(root, selected)).filter((dir) => isInitialized({ path: dir }))])];
16
+ if (matches.length > 1)
17
+ throw new Error("More than one registered directory has this name. Select its registry ID.");
18
+ return matches[0] ?? path.join(path.dirname(fallback), selected);
19
+ }
20
+ 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 }))];
21
+ return candidates.find((dir) => isInitialized({ path: dir })) ?? fallback;
22
+ }
23
+ //# sourceMappingURL=workspace-directory.js.map
@@ -1,8 +1,20 @@
1
+ import { type ClientRequest, type IncomingMessage } from "node:http";
2
+ import { type RequestOptions } from "node:https";
1
3
  import type { HttpRequest, HttpResponse } from "@jini-ai/core/primitives";
2
4
  import type { PinnedPeer } from "./types.js";
3
5
  import type { HttpTransportAdapter } from "./ports.js";
6
+ /** Native request effect port; peer pinning and TLS options remain owned by this adapter. */
7
+ export interface NativeHttpTransportOptions {
8
+ /** Like node:http, invoke onResponse asynchronously, after returning the request handle. */
9
+ request?: (input: {
10
+ secure: boolean;
11
+ options: RequestOptions;
12
+ onResponse: (response: IncomingMessage) => void;
13
+ }, optional?: Record<string, never>) => ClientRequest;
14
+ }
4
15
  export declare class FetchHttpTransportAdapter implements HttpTransportAdapter {
5
- constructor(_required: Record<string, never>);
16
+ private readonly optional;
17
+ constructor(_required: Record<string, never>, optional?: NativeHttpTransportOptions);
6
18
  requestPinned({ request: req, peer }: {
7
19
  request: HttpRequest;
8
20
  peer: PinnedPeer;
@@ -11,7 +11,10 @@ import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
11
11
  /** Last-resort decoded-byte bound independent of any caller's response policy. */
12
12
  const ABSOLUTE_MAX_BYTES = 100 * 1024 * 1024;
13
13
  export class FetchHttpTransportAdapter {
14
- constructor(_required) { }
14
+ optional;
15
+ constructor(_required, optional = {}) {
16
+ this.optional = optional;
17
+ }
15
18
  async requestPinned({ request: req, peer }) {
16
19
  const url = new URL(req.url);
17
20
  const isHttps = url.protocol === "https:";
@@ -31,7 +34,7 @@ export class FetchHttpTransportAdapter {
31
34
  ...(isHttps && peer.tlsServerName ? { servername: peer.tlsServerName } : {}),
32
35
  };
33
36
  return new Promise((resolve, reject) => {
34
- const clientRequest = requestFn(options, (res) => {
37
+ const onResponse = (res) => {
35
38
  // HEAD can advertise GET's compression but has no body to decompress.
36
39
  const decoded = req.method === "HEAD" ? res : decodeBody(res.headers["content-encoding"], res);
37
40
  const chunks = [];
@@ -74,7 +77,10 @@ export class FetchHttpTransportAdapter {
74
77
  });
75
78
  decoded.on("end", () => finish(false));
76
79
  decoded.on("error", reject);
77
- });
80
+ };
81
+ const clientRequest = this.optional.request
82
+ ? this.optional.request({ secure: isHttps, options, onResponse }, {})
83
+ : requestFn(options, onResponse);
78
84
  clientRequest.on("timeout", () => {
79
85
  clientRequest.destroy(new Error(`request timed out after ${idleTimeoutMs}ms`));
80
86
  });
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * - `fs` — filesystem containment, atomic copy, removal, log tails.
11
11
  * - `http` — HTTP readiness polling.
12
12
  * - `fetch-with-timeout` — the shared `AbortSignal`-timeout wrapper every outbound `fetch()` call
13
- * in this monorepo is meant to go through (2026-08-16 failure-mode audit, Finding 2).
13
+ * in this monorepo is meant to go through.
14
14
  * - `toolchain` — user-level toolchain bin discovery.
15
15
  * - `asset-cache` — SSRF-safe same-origin cache/proxy for sandboxed content's
16
16
  * external media references.
@@ -26,62 +26,14 @@
26
26
  * - `blob-storage` — a backend-agnostic blob storage port + local-disk and
27
27
  * S3-compatible implementations.
28
28
  *
29
- * The set of names exported here is intentionally identical to the pre-split
30
- * public surface; importers see no change.
31
29
 
32
- * Archived provenance rationale:
33
- * ## Design decision: `project-storage.ts` was split, not ported wholesale or dropped
34
- *
35
- * The task brief asked to verify the recon's "leans OD" flag on
36
- * `project-storage.ts` firsthand and either drop it or extract a generic core
37
- * with OD's project-specific parts as an adapter, "depending on what you
38
- * actually find." Read in full: the `ProjectStorage` interface,
39
- * `LocalProjectStorage`, and `S3ProjectStorage` classes carry no OD nouns in
40
- * their own logic — they're a backend-agnostic blob CRUD port (read/write/
41
- * list/delete/stat under a scoping key) plus a local-disk and an
42
- * S3-compatible implementation, both traversal-guarded. The **only** OD
43
- * coupling in the file is the bottom `resolveProjectStorage()` factory
44
- * function, which reads `OD_PROJECT_STORAGE`/`OD_S3_BUCKET`/`OD_S3_REGION`/
45
- * `OD_S3_PREFIX`/`OD_S3_ENDPOINT`/`OD_S3_ACCESS_KEY_ID`/
46
- * `OD_S3_SECRET_ACCESS_KEY`/`OD_S3_SESSION_TOKEN` directly from `process.env`.
47
- *
48
- * **Extracted, not dropped:** the interface + both implementations, ported to
49
- * `src/blob-storage.ts` as `BlobStorage`/`BlobFileMeta`/`LocalBlobStorage`/
50
- * `S3BlobStorage`/`S3BlobStorageOptions`/`StorageError`, with every
51
- * `projectId: string` parameter renamed to `namespace: string` — per
52
- * extraction-plan.md's Task-4 Port-2 finding ("OD's 'project' is a product
53
- * model; engine needs a generic workspace/session store interface") and §2.1's
54
- * "Runs key on an opaque `contextRef`, never `projectId`" convention, "project"
55
- * specifically is flagged elsewhere in this porting effort as OD's noun, not
56
- * the engine's — `namespace` carries no domain meaning here beyond "a
57
- * top-level grouping/scoping key," which is what the parameter actually is
58
- * structurally (nothing in `LocalBlobStorage`/`S3BlobStorage`'s logic assumes
59
- * it means a design project).
60
- *
61
- * **Dropped, not ported:** `resolveProjectStorage()`. This is OD adapter
62
- * wiring — a specific env-var-naming convention and the choice to read
63
- * `process.env` directly inside the engine layer — not generic engine
64
- * behavior. It has no equivalent anywhere else in this porting session's
65
- * established pattern (`createSqliteEventLog`, `LocalBlobStorage`,
66
- * `S3BlobStorage` are all called with explicit constructor arguments; none of
67
- * `@jini/*`'s existing adapters read `process.env` inside their own
68
- * constructors). A Jini host application composes its own equivalent
69
- * (`new LocalBlobStorage(root)` or `new S3BlobStorage({ bucket, region,
70
- * credentials, ... })` called directly, with whatever env-var convention that
71
- * host prefers) rather than inheriting OD's `OD_S3_*` names or its
72
- * env-selection shape.
73
- *
74
- * Landed in `@jini/platform`, not `@jini/sqlite`: this is a filesystem/network
75
- * blob-storage primitive with no SQL involvement, parallel to this package's
76
- * existing `fs.ts` (path containment / atomic copy) and `http.ts` (readiness
77
- * polling) roles — `@jini/sqlite` is reserved for the `EventLog` port adapter
78
- * and `better-sqlite3`-specific helpers (`db-inspect.ts`,
79
- * `backend-config.ts`), none of which this needs. `aws-sigv4.ts` lives
80
- * alongside it in the same package since it's `S3BlobStorage`'s only
81
- * dependency and has no other consumer yet.
30
+ * Blob storage uses opaque namespace scoping rather than a product's project model. Hosts supply
31
+ * configuration/credentials explicitly instead of inheriting product-prefixed environment names.
32
+ * Filesystem/network storage and its SigV4 signing belong here, with no kernel SQL-port dependency.
82
33
  */
83
34
  export type { CommandInvocation, CommandInvocationRequest } from "./command.js";
84
35
  export { createCommandInvocation, createPackageManagerInvocation } from "./command.js";
36
+ export { omitEnvironmentKeys } from "./child-environment.js";
85
37
  export type { ResolveSystemProxyEnvOptions, SystemProxyCommandRunner } from "./proxy-env.js";
86
38
  export { mergeProxyAwareEnv, parseMacosScutilProxyOutput, parseWindowsInternetSettingsProxyOutput, resolveSystemProxyEnv, } from "./proxy-env.js";
87
39
  export type { ProcessSnapshot, ProcessStampContract, ProcessStampField, ProcessStampShape, SpawnProcessRequest, StampedProcessMatchCriteria, StopProcessesResult, } from "./process.js";
@@ -97,7 +49,7 @@ export { wellKnownUserToolchainBins } from "./toolchain.js";
97
49
  export type { BufferedCommandResult } from "./shell.js";
98
50
  export { execCommandViaLoginShell, execFileBuffered } from "./shell.js";
99
51
  export type { AssetCache, AssetCacheOptions, AssetCacheResult } from "./asset-cache.js";
100
- export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createValidatingLookup, isCacheableExternalUrl, } from "./asset-cache.js";
52
+ export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createAssetCacheDispatcher, createValidatingLookup, isCacheableExternalUrl, } from "./asset-cache.js";
101
53
  export { expandHomePrefix, resolveProjectRelativePath } from "./home-expansion.js";
102
54
  export type { SandboxEnvConfig, SandboxRuntimeConfig, SandboxRuntimeRoots } from "./sandbox-env.js";
103
55
  export { applySandboxRuntimeEnv, ensureSandboxRuntimeDirs, isSandboxImportedProjectRootAllowed, isSandboxModeEnabled, resolveSandboxRuntimeConfig, resolveSandboxRuntimeConfigFromEnv, sandboxAgentProfilesConfigPath, sandboxImportAllowedRoots, sandboxImportedProjectRootUnavailableReason, } from "./sandbox-env.js";
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * - `fs` — filesystem containment, atomic copy, removal, log tails.
11
11
  * - `http` — HTTP readiness polling.
12
12
  * - `fetch-with-timeout` — the shared `AbortSignal`-timeout wrapper every outbound `fetch()` call
13
- * in this monorepo is meant to go through (2026-08-16 failure-mode audit, Finding 2).
13
+ * in this monorepo is meant to go through.
14
14
  * - `toolchain` — user-level toolchain bin discovery.
15
15
  * - `asset-cache` — SSRF-safe same-origin cache/proxy for sandboxed content's
16
16
  * external media references.
@@ -26,61 +26,13 @@
26
26
  * - `blob-storage` — a backend-agnostic blob storage port + local-disk and
27
27
  * S3-compatible implementations.
28
28
  *
29
- * The set of names exported here is intentionally identical to the pre-split
30
- * public surface; importers see no change.
31
29
 
32
- * Archived provenance rationale:
33
- * ## Design decision: `project-storage.ts` was split, not ported wholesale or dropped
34
- *
35
- * The task brief asked to verify the recon's "leans OD" flag on
36
- * `project-storage.ts` firsthand and either drop it or extract a generic core
37
- * with OD's project-specific parts as an adapter, "depending on what you
38
- * actually find." Read in full: the `ProjectStorage` interface,
39
- * `LocalProjectStorage`, and `S3ProjectStorage` classes carry no OD nouns in
40
- * their own logic — they're a backend-agnostic blob CRUD port (read/write/
41
- * list/delete/stat under a scoping key) plus a local-disk and an
42
- * S3-compatible implementation, both traversal-guarded. The **only** OD
43
- * coupling in the file is the bottom `resolveProjectStorage()` factory
44
- * function, which reads `OD_PROJECT_STORAGE`/`OD_S3_BUCKET`/`OD_S3_REGION`/
45
- * `OD_S3_PREFIX`/`OD_S3_ENDPOINT`/`OD_S3_ACCESS_KEY_ID`/
46
- * `OD_S3_SECRET_ACCESS_KEY`/`OD_S3_SESSION_TOKEN` directly from `process.env`.
47
- *
48
- * **Extracted, not dropped:** the interface + both implementations, ported to
49
- * `src/blob-storage.ts` as `BlobStorage`/`BlobFileMeta`/`LocalBlobStorage`/
50
- * `S3BlobStorage`/`S3BlobStorageOptions`/`StorageError`, with every
51
- * `projectId: string` parameter renamed to `namespace: string` — per
52
- * extraction-plan.md's Task-4 Port-2 finding ("OD's 'project' is a product
53
- * model; engine needs a generic workspace/session store interface") and §2.1's
54
- * "Runs key on an opaque `contextRef`, never `projectId`" convention, "project"
55
- * specifically is flagged elsewhere in this porting effort as OD's noun, not
56
- * the engine's — `namespace` carries no domain meaning here beyond "a
57
- * top-level grouping/scoping key," which is what the parameter actually is
58
- * structurally (nothing in `LocalBlobStorage`/`S3BlobStorage`'s logic assumes
59
- * it means a design project).
60
- *
61
- * **Dropped, not ported:** `resolveProjectStorage()`. This is OD adapter
62
- * wiring — a specific env-var-naming convention and the choice to read
63
- * `process.env` directly inside the engine layer — not generic engine
64
- * behavior. It has no equivalent anywhere else in this porting session's
65
- * established pattern (`createSqliteEventLog`, `LocalBlobStorage`,
66
- * `S3BlobStorage` are all called with explicit constructor arguments; none of
67
- * `@jini/*`'s existing adapters read `process.env` inside their own
68
- * constructors). A Jini host application composes its own equivalent
69
- * (`new LocalBlobStorage(root)` or `new S3BlobStorage({ bucket, region,
70
- * credentials, ... })` called directly, with whatever env-var convention that
71
- * host prefers) rather than inheriting OD's `OD_S3_*` names or its
72
- * env-selection shape.
73
- *
74
- * Landed in `@jini/platform`, not `@jini/sqlite`: this is a filesystem/network
75
- * blob-storage primitive with no SQL involvement, parallel to this package's
76
- * existing `fs.ts` (path containment / atomic copy) and `http.ts` (readiness
77
- * polling) roles — `@jini/sqlite` is reserved for the `EventLog` port adapter
78
- * and `better-sqlite3`-specific helpers (`db-inspect.ts`,
79
- * `backend-config.ts`), none of which this needs. `aws-sigv4.ts` lives
80
- * alongside it in the same package since it's `S3BlobStorage`'s only
81
- * dependency and has no other consumer yet.
30
+ * Blob storage uses opaque namespace scoping rather than a product's project model. Hosts supply
31
+ * configuration/credentials explicitly instead of inheriting product-prefixed environment names.
32
+ * Filesystem/network storage and its SigV4 signing belong here, with no kernel SQL-port dependency.
82
33
  */
83
34
  export { createCommandInvocation, createPackageManagerInvocation } from "./command.js";
35
+ export { omitEnvironmentKeys } from "./child-environment.js";
84
36
  export { mergeProxyAwareEnv, parseMacosScutilProxyOutput, parseWindowsInternetSettingsProxyOutput, resolveSystemProxyEnv, } from "./proxy-env.js";
85
37
  export { collectProcessTreePids, createProcessStampArgs, isProcessAlive, listProcessSnapshots, matchesProcessStamp, matchesStampedProcess, readFlagValue, readProcessStamp, readProcessStampFromCommand, spawnBackgroundProcess, spawnLoggedProcess, stopProcesses, waitForProcessExit, } from "./process.js";
86
38
  export { atomicCopyFile, pathContains, readLogTail, removePathBestEffort } from "./fs.js";
@@ -88,7 +40,7 @@ export { waitForHttpOk } from "./http.js";
88
40
  export { FETCH_TIMEOUT_MS, FetchTimeoutError, fetchWithTimeout } from "./fetch-with-timeout.js";
89
41
  export { wellKnownUserToolchainBins } from "./toolchain.js";
90
42
  export { execCommandViaLoginShell, execFileBuffered } from "./shell.js";
91
- export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createValidatingLookup, isCacheableExternalUrl, } from "./asset-cache.js";
43
+ export { AssetCacheError, assertSafePublicUrl, assetCacheKey, assetCacheRewriteUrl, createAssetCache, createAssetCacheDispatcher, createValidatingLookup, isCacheableExternalUrl, } from "./asset-cache.js";
92
44
  export { expandHomePrefix, resolveProjectRelativePath } from "./home-expansion.js";
93
45
  export { applySandboxRuntimeEnv, ensureSandboxRuntimeDirs, isSandboxImportedProjectRootAllowed, isSandboxModeEnabled, resolveSandboxRuntimeConfig, resolveSandboxRuntimeConfigFromEnv, sandboxAgentProfilesConfigPath, sandboxImportAllowedRoots, sandboxImportedProjectRootUnavailableReason, } from "./sandbox-env.js";
94
46
  export { resolveDaemonCliPath, resolveDaemonPluginPreviewsDir, resolveDaemonResourceDir, resolveDaemonResourceRoot, resolveDataDir, resolveProcessResourcesPath, } from "./resource-paths.js";
@@ -23,14 +23,7 @@ export declare function expandIpv6({ address: addr }: {
23
23
  export declare function isPrivateAddress({ address: addr }: {
24
24
  address: string;
25
25
  }): boolean;
26
- /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
27
- export declare function isLoopbackApiHost({ hostname }: {
28
- hostname: string;
29
- }): boolean;
30
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
31
- export declare function isBlockedExternalApiHostname({ hostname }: {
32
- hostname: string;
33
- }): boolean;
26
+ export { isLoopbackApiHost, isBlockedExternalApiHostname } from './endpoint-policy.js';
34
27
  /** True when `hostname` is `localhost`, an IPv6 loopback literal, or a `127.0.0.0/8` IPv4 literal. */
35
28
  export declare function isLoopbackHostname({ hostname }: {
36
29
  readonly hostname: unknown;