@jini-ai/platform 0.1.2 → 0.5.0

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 (128) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +207 -0
  3. package/dist/asset-cache.d.ts +0 -22
  4. package/dist/asset-cache.js +3 -138
  5. package/dist/download.js +2 -2
  6. package/dist/fetch-with-timeout.d.ts +105 -0
  7. package/dist/fetch-with-timeout.js +121 -0
  8. package/dist/fs/atomic-write.d.ts +103 -0
  9. package/dist/fs/atomic-write.js +201 -0
  10. package/dist/fs/durable-json.d.ts +89 -0
  11. package/dist/fs/durable-json.js +166 -0
  12. package/dist/fs/env-text.d.ts +11 -0
  13. package/dist/fs/env-text.js +22 -0
  14. package/dist/fs/file-lock.d.ts +126 -0
  15. package/dist/fs/file-lock.js +306 -0
  16. package/dist/fs/guarded-reader.d.ts +60 -0
  17. package/dist/fs/guarded-reader.js +310 -0
  18. package/dist/fs/index.d.ts +8 -0
  19. package/dist/fs/index.js +9 -0
  20. package/dist/fs/strict-containment.d.ts +5 -0
  21. package/dist/fs/strict-containment.js +13 -0
  22. package/dist/fs.js +2 -3
  23. package/dist/http/guarded/client.d.ts +13 -0
  24. package/dist/http/guarded/client.js +240 -0
  25. package/dist/http/guarded/errors.d.ts +21 -0
  26. package/dist/http/guarded/errors.js +20 -0
  27. package/dist/http/guarded/fetch-adapter.d.ts +23 -0
  28. package/dist/http/guarded/fetch-adapter.js +30 -0
  29. package/dist/http/guarded/index.d.ts +12 -0
  30. package/dist/http/guarded/index.js +7 -0
  31. package/dist/http/guarded/node-ports.d.ts +8 -0
  32. package/dist/http/guarded/node-ports.js +11 -0
  33. package/dist/http/guarded/ports.d.ts +51 -0
  34. package/dist/http/guarded/ports.js +2 -0
  35. package/dist/http/guarded/transport.fetch.d.ts +11 -0
  36. package/dist/http/guarded/transport.fetch.js +109 -0
  37. package/dist/http/guarded/types.d.ts +8 -0
  38. package/dist/http/guarded/types.js +2 -0
  39. package/dist/http.js +10 -1
  40. package/dist/index.d.ts +65 -1
  41. package/dist/index.js +64 -1
  42. package/dist/mail/adapter-module.d.ts +20 -0
  43. package/dist/mail/adapter-module.js +2 -0
  44. package/dist/mail/delivery-availability.d.ts +7 -0
  45. package/dist/mail/delivery-availability.js +6 -0
  46. package/dist/mail/index.d.ts +7 -0
  47. package/dist/mail/index.js +3 -0
  48. package/dist/mail/ports.d.ts +43 -0
  49. package/dist/mail/ports.js +2 -0
  50. package/dist/mail/purpose-scoped-mailer.d.ts +18 -0
  51. package/dist/mail/purpose-scoped-mailer.js +50 -0
  52. package/dist/mail/smtp.d.ts +79 -0
  53. package/dist/mail/smtp.js +102 -0
  54. package/dist/mail/types.d.ts +83 -0
  55. package/dist/mail/types.js +2 -0
  56. package/dist/messages.d.ts +22 -0
  57. package/dist/messages.js +15 -0
  58. package/dist/net/address.d.ts +38 -0
  59. package/dist/net/address.js +241 -0
  60. package/dist/net/index.d.ts +2 -0
  61. package/dist/net/index.js +2 -0
  62. package/dist/process.d.ts +2 -1
  63. package/dist/process.js +2 -1
  64. package/dist/proxy-env.js +1 -1
  65. package/dist/resource-paths.js +1 -1
  66. package/dist/sandbox-env.d.ts +1 -1
  67. package/dist/sandbox-env.js +1 -1
  68. package/dist/secrets/aad.d.ts +7 -0
  69. package/dist/secrets/aad.js +5 -0
  70. package/dist/secrets/credential-sets/aad.d.ts +7 -0
  71. package/dist/secrets/credential-sets/aad.js +7 -0
  72. package/dist/secrets/credential-sets/index.d.ts +4 -0
  73. package/dist/secrets/credential-sets/index.js +3 -0
  74. package/dist/secrets/credential-sets/repo.memory.d.ts +36 -0
  75. package/dist/secrets/credential-sets/repo.memory.js +72 -0
  76. package/dist/secrets/credential-sets/types.d.ts +67 -0
  77. package/dist/secrets/credential-sets/types.js +6 -0
  78. package/dist/secrets/index.d.ts +9 -0
  79. package/dist/secrets/index.js +5 -0
  80. package/dist/secrets/keyring.env.d.ts +176 -0
  81. package/dist/secrets/keyring.env.js +271 -0
  82. package/dist/secrets/keyring.memory.d.ts +10 -0
  83. package/dist/secrets/keyring.memory.js +9 -0
  84. package/dist/secrets/ports.d.ts +33 -0
  85. package/dist/secrets/ports.js +2 -0
  86. package/dist/secrets/secret-sealer.aesgcm.d.ts +22 -0
  87. package/dist/secrets/secret-sealer.aesgcm.js +64 -0
  88. package/dist/secrets/testing.d.ts +4 -0
  89. package/dist/secrets/testing.js +4 -0
  90. package/dist/secrets/types.d.ts +10 -0
  91. package/dist/secrets/types.js +2 -0
  92. package/dist/shell.d.ts +10 -6
  93. package/dist/shell.js +10 -6
  94. package/dist/terminal.d.ts +2 -2
  95. package/dist/terminal.js +9 -5
  96. package/package.json +85 -4
  97. package/dist/asset-cache.d.ts.map +0 -1
  98. package/dist/asset-cache.js.map +0 -1
  99. package/dist/aws-sigv4.d.ts.map +0 -1
  100. package/dist/aws-sigv4.js.map +0 -1
  101. package/dist/blob-storage.d.ts.map +0 -1
  102. package/dist/blob-storage.js.map +0 -1
  103. package/dist/command.d.ts.map +0 -1
  104. package/dist/command.js.map +0 -1
  105. package/dist/download.d.ts.map +0 -1
  106. package/dist/download.js.map +0 -1
  107. package/dist/fs.d.ts.map +0 -1
  108. package/dist/fs.js.map +0 -1
  109. package/dist/home-expansion.d.ts.map +0 -1
  110. package/dist/home-expansion.js.map +0 -1
  111. package/dist/http.d.ts.map +0 -1
  112. package/dist/http.js.map +0 -1
  113. package/dist/index.d.ts.map +0 -1
  114. package/dist/index.js.map +0 -1
  115. package/dist/process.d.ts.map +0 -1
  116. package/dist/process.js.map +0 -1
  117. package/dist/proxy-env.d.ts.map +0 -1
  118. package/dist/proxy-env.js.map +0 -1
  119. package/dist/resource-paths.d.ts.map +0 -1
  120. package/dist/resource-paths.js.map +0 -1
  121. package/dist/sandbox-env.d.ts.map +0 -1
  122. package/dist/sandbox-env.js.map +0 -1
  123. package/dist/shell.d.ts.map +0 -1
  124. package/dist/shell.js.map +0 -1
  125. package/dist/terminal.d.ts.map +0 -1
  126. package/dist/terminal.js.map +0 -1
  127. package/dist/toolchain.d.ts.map +0 -1
  128. package/dist/toolchain.js.map +0 -1
@@ -0,0 +1,60 @@
1
+ import nodeFs from "node:fs";
2
+ /** Refusal of a malformed, denied, escaped, or over-limit file operation. */
3
+ export declare class FsFilePathError extends Error {
4
+ constructor({ message }: {
5
+ message: string;
6
+ });
7
+ }
8
+ export interface DenyRules {
9
+ segments: ReadonlySet<string>;
10
+ filenamePatterns: readonly RegExp[];
11
+ }
12
+ export interface GuardedReaderLimits {
13
+ maxFileBytes: number;
14
+ binarySniffBytes: number;
15
+ maxListedFiles: number;
16
+ maxWalkDepth: number;
17
+ maxWalkEntries: number;
18
+ }
19
+ export type GuardedReaderFilesystem = Pick<typeof nodeFs, "existsSync" | "lstatSync" | "readdirSync" | "realpathSync" | "statSync" | "openSync" | "readSync" | "closeSync" | "createReadStream">;
20
+ /** Native filesystem adapter; callers can replace individual operations for a virtual filesystem. */
21
+ export declare function createNodeGuardedReaderFilesystem(_required: Record<string, never>): GuardedReaderFilesystem;
22
+ export interface GuardedFileReader {
23
+ resolve(required: {
24
+ relativePath: string;
25
+ }): string;
26
+ read(required: {
27
+ relativePath: string;
28
+ }): {
29
+ content: string;
30
+ bytes: number;
31
+ };
32
+ readBytes(required: {
33
+ relativePath: string;
34
+ maxBytes: number;
35
+ }): Promise<Uint8Array>;
36
+ list(required: Record<string, never>, optional?: {
37
+ relativePath?: string;
38
+ }): {
39
+ files: string[];
40
+ truncated: boolean;
41
+ };
42
+ isDeniedFileName(required: {
43
+ fileName: string;
44
+ }): boolean;
45
+ isDeniedPathSegment(required: {
46
+ segmentName: string;
47
+ }): boolean;
48
+ }
49
+ /** Contains both requested and resolved paths, including symlink aliases and nonexistent descendants.
50
+ * Deny rules and traversal/read limits are host policy and have no implicit defaults.
51
+ */
52
+ export declare function createGuardedFileReader({ rootPath, denyRules, limits, filesystem: fs }: {
53
+ rootPath: string;
54
+ denyRules: DenyRules;
55
+ limits: GuardedReaderLimits;
56
+ filesystem: GuardedReaderFilesystem;
57
+ }, { excludedListingDirs }?: {
58
+ excludedListingDirs?: ReadonlySet<string>;
59
+ }): GuardedFileReader;
60
+ //# sourceMappingURL=guarded-reader.d.ts.map
@@ -0,0 +1,310 @@
1
+ import nodeFs from "node:fs";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ // A distinct refusal class lets tool adapters treat rejected paths/content as actionable input
4
+ // problems instead of redacting them as an internal crash.
5
+ /** Refusal of a malformed, denied, escaped, or over-limit file operation. */
6
+ export class FsFilePathError extends Error {
7
+ constructor({ message }) { super(message); this.name = "FsFilePathError"; }
8
+ }
9
+ /** Native filesystem adapter; callers can replace individual operations for a virtual filesystem. */
10
+ export function createNodeGuardedReaderFilesystem(_required) { return nodeFs; }
11
+ /** Contains both requested and resolved paths, including symlink aliases and nonexistent descendants.
12
+ * Deny rules and traversal/read limits are host policy and have no implicit defaults.
13
+ */
14
+ export function createGuardedFileReader({ rootPath, denyRules, limits, filesystem: fs }, { excludedListingDirs = new Set(["node_modules", ".git", "dist"]) } = {}) {
15
+ if (!rootPath)
16
+ throw new TypeError("rootPath is required");
17
+ for (const value of Object.values(limits))
18
+ if (!Number.isSafeInteger(value) || value <= 0)
19
+ throw new TypeError("reader limits must be positive safe integers");
20
+ const { existsSync, lstatSync, readdirSync, realpathSync, statSync } = fs;
21
+ const MAX_FS_FILE_BYTES = limits.maxFileBytes, BINARY_SNIFF_BYTES = limits.binarySniffBytes;
22
+ const MAX_LISTED_FILES = limits.maxListedFiles, MAX_WALK_DEPTH = limits.maxWalkDepth, MAX_WALK_ENTRIES = limits.maxWalkEntries;
23
+ // Listing exclusions reduce noise; they never grant or deny a known file read. Security comes
24
+ // from denyRules, applied to both the caller spelling and the effective target.
25
+ const EXCLUDED_LISTING_DIR_NAMES = excludedListingDirs;
26
+ function isDeniedFsFileName(fileName) {
27
+ const folded = foldFsNameForDenylist(fileName);
28
+ return denyRules.filenamePatterns.some((pattern) => new RegExp(pattern).test(fileName) || new RegExp(pattern).test(folded));
29
+ }
30
+ function isDeniedFsPathSegment(segmentName) {
31
+ return denyRules.segments.has(segmentName.toLowerCase()) || denyRules.segments.has(foldFsNameForDenylist(segmentName));
32
+ }
33
+ // Default case-insensitive APFS can open LONG S/KELVIN SIGN aliases of denied names that plain
34
+ // toLowerCase or non-Unicode /i misses. Windows also ignores trailing dots/spaces and opens
35
+ // name::$DATA as name. Compare raw AND folded names: folding may add denials, never remove them,
36
+ // and must not change the path actually opened.
37
+ function foldFsNameForDenylist(name) {
38
+ const withoutStream = name.replace(/:[^]*$/, "");
39
+ const withoutTrailing = withoutStream.replace(/[. ]+$/, "");
40
+ return withoutTrailing.toLowerCase().toUpperCase().toLowerCase();
41
+ }
42
+ // Split both separator spellings even on a host whose native separator is '/', so deny checks
43
+ // remain cross-platform. The guard and reader must resolve the same spelling consistently.
44
+ function normalizeFsRelativePath(relativePath) {
45
+ const segments = [];
46
+ for (const segment of relativePath.split(/[\\/]/)) {
47
+ if (segment === "" || segment === ".")
48
+ continue;
49
+ if (segment === "..") {
50
+ segments.pop();
51
+ continue;
52
+ }
53
+ segments.push(segment);
54
+ }
55
+ return segments.join("/");
56
+ }
57
+ // A string prefix admits siblings whose names extend the root and ignores traversal. Resolve
58
+ // first, then compare path.relative components; symlink containment needs its own check below.
59
+ function isWithin(base, target) {
60
+ if (base === target)
61
+ return true;
62
+ const rel = relative(base, target);
63
+ return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
64
+ }
65
+ // Probe the deepest existing ancestor: a nonexistent descendant can still sit beneath a
66
+ // symlink that escapes the root, even when its lexical path passed containment.
67
+ // Bound ancestor steps to four times the listing depth, plus the endpoint check, so deeply
68
+ // nested missing paths cannot cause unbounded synchronous probes. Unknown containment refuses.
69
+ function assertNoSymlinkEscape(base, path, relativePathForError) {
70
+ let probe = path;
71
+ for (let i = 0; i < MAX_WALK_DEPTH * 4 && !existsSync(probe); i += 1) {
72
+ const parent = dirname(probe);
73
+ if (parent === probe)
74
+ break;
75
+ probe = parent;
76
+ }
77
+ if (!existsSync(probe))
78
+ throw new FsFilePathError({ message: `path '${relativePathForError}' containment could not be established within the ancestor traversal limit` });
79
+ const realProbe = realpathSync(probe);
80
+ if (!isWithin(base, realProbe)) {
81
+ throw new FsFilePathError({ message: `path '${relativePathForError}' resolves outside the allowed root through a symbolic link` });
82
+ }
83
+ }
84
+ function resolveFsFilePath(required) {
85
+ const { rootPath, relativePath } = required;
86
+ assertWellFormedRelativePath(relativePath);
87
+ assertRequestedPathNotDenied(relativePath);
88
+ // Canonicalize a symlinked root too, or the ancestor's realpath would compare to a different
89
+ // representation and refuse legitimate reads through a linked package/root.
90
+ const base = existsSync(rootPath) ? realpathSync(rootPath) : resolve(rootPath);
91
+ const target = resolve(base, relativePath);
92
+ if (!isWithin(base, target)) {
93
+ throw new FsFilePathError({ message: `path '${relativePath}' resolves outside the allowed root` });
94
+ }
95
+ assertNoSymlinkEscape(base, target, relativePath);
96
+ assertEffectiveTargetNotDenied(base, target, relativePath);
97
+ return target;
98
+ }
99
+ // Isolate initial shape refusals so resolve reads as the ordered sequence of containment and
100
+ // deny checks, rather than burying that boundary in one long guard block.
101
+ function assertWellFormedRelativePath(relativePath) {
102
+ if (relativePath.length === 0) {
103
+ throw new FsFilePathError({ message: "path is required" });
104
+ }
105
+ if (relativePath.includes("\0")) {
106
+ throw new FsFilePathError({ message: "path must not contain a NUL byte" });
107
+ }
108
+ if (isAbsolute(relativePath)) {
109
+ throw new FsFilePathError({ message: `path '${relativePath}' must be relative to the root, not absolute` });
110
+ }
111
+ }
112
+ // Refuse denied spellings before filesystem access, regardless of whether they exist. The
113
+ // effective-target check is separate because a harmless alias can resolve to a denied file.
114
+ function assertRequestedPathNotDenied(relativePath) {
115
+ const normalized = normalizeFsRelativePath(relativePath);
116
+ const segments = normalized.length === 0 ? [] : normalized.split("/");
117
+ for (const segment of segments) {
118
+ if (isDeniedFsPathSegment(segment)) {
119
+ throw new FsFilePathError({ message: `path '${relativePath}' contains a denied path segment ('${segment}') and cannot be accessed` });
120
+ }
121
+ }
122
+ const leafName = segments.at(-1) ?? "";
123
+ if (leafName.length > 0 && isDeniedFsFileName(leafName)) {
124
+ throw new FsFilePathError({ message: `path '${relativePath}' matches a denied filename pattern and cannot be accessed` });
125
+ }
126
+ }
127
+ // public.txt -> .env stays inside the root and passes lexical checks; Node follows it on read.
128
+ // Deny the real target as well. existsSync excludes broken/circular links before realpath, and
129
+ // the subsequent stat translates their failure into this reader's own refusal class.
130
+ function assertEffectiveTargetNotDenied(base, target, relativePath) {
131
+ if (!existsSync(target))
132
+ return;
133
+ const effectiveSegments = relative(base, realpathSync(target)).split(sep);
134
+ for (const segment of effectiveSegments) {
135
+ if (isDeniedFsPathSegment(segment)) {
136
+ throw new FsFilePathError({ message: `path '${relativePath}' resolves to a denied path segment ('${segment}') and cannot be accessed` });
137
+ }
138
+ }
139
+ // A target equal to base yields [""] after split, so check leaf length, not array length.
140
+ const effectiveLeaf = effectiveSegments.at(-1) ?? "";
141
+ if (effectiveLeaf.length > 0 && isDeniedFsFileName(effectiveLeaf)) {
142
+ throw new FsFilePathError({ message: `path '${relativePath}' resolves to a denied filename pattern and cannot be accessed` });
143
+ }
144
+ }
145
+ function visitFsDirEntry(dir, name, depth, base, found, state) {
146
+ const full = resolve(dir, name);
147
+ // lstat examines the link itself: stat would follow circular links and throw ELOOP before
148
+ // the skip. Never enumerate/report links or descend denied directories, so listings do not
149
+ // advertise secrets or anything outside the root. Ordinary excluded directories are noise.
150
+ const stat = lstatSync(full, { throwIfNoEntry: false });
151
+ if (!stat)
152
+ return;
153
+ if (stat.isSymbolicLink())
154
+ return;
155
+ if (stat.isDirectory()) {
156
+ if (isDeniedFsPathSegment(name) || EXCLUDED_LISTING_DIR_NAMES.has(name))
157
+ return;
158
+ walkFsDir(full, depth + 1, base, found, state);
159
+ return;
160
+ }
161
+ if (!stat.isFile())
162
+ return;
163
+ if (isDeniedFsFileName(name))
164
+ return;
165
+ found.push(relative(base, full).split(sep).join("/"));
166
+ }
167
+ // Results, entries, and depth are independent limits; keep the decision shared by entry and
168
+ // loop checks so adding one limit cannot leave an unbounded arm.
169
+ function walkBoundReached(found, state, depth) {
170
+ return depth > MAX_WALK_DEPTH || found.length >= MAX_LISTED_FILES || state.remaining <= 0;
171
+ }
172
+ function walkFsDir(dir, depth, base, found, state) {
173
+ if (walkBoundReached(found, state, depth)) {
174
+ state.truncated = true;
175
+ return;
176
+ }
177
+ for (const name of readdirSync(dir)) {
178
+ // Recheck per entry: this loop spends traversal budget and adds results. Remaining
179
+ // entries mean a bound must set truncated rather than present a short list as complete.
180
+ if (walkBoundReached(found, state, depth)) {
181
+ state.truncated = true;
182
+ return;
183
+ }
184
+ state.remaining -= 1;
185
+ visitFsDirEntry(dir, name, depth, base, found, state);
186
+ }
187
+ }
188
+ function listFsFiles(required) {
189
+ const { rootPath } = required;
190
+ const relativePath = required.relativePath ?? "";
191
+ const startDir = relativePath.length === 0 ? resolve(rootPath) : resolveFsFilePath({ rootPath, relativePath });
192
+ // Freshly provisioned roots/subdirectories may not exist yet; an empty listing is valid.
193
+ if (!existsSync(startDir))
194
+ return { files: [], truncated: false };
195
+ const startStat = statOrFsPathError(startDir, relativePath || ".");
196
+ if (!startStat)
197
+ return { files: [], truncated: false };
198
+ if (!startStat.isDirectory()) {
199
+ throw new FsFilePathError({ message: `path '${relativePath || "."}' is not a directory` });
200
+ }
201
+ const base = realpathSync(startDir);
202
+ const found = [];
203
+ const state = { remaining: MAX_WALK_ENTRIES, truncated: false };
204
+ walkFsDir(base, 0, base, found, state);
205
+ // Paths are relative to the requested walk base, so a subdirectory listing can be consumed
206
+ // without stripping that subdirectory's prefix a second time.
207
+ return { files: found.sort(), truncated: state.truncated };
208
+ }
209
+ // Circular-link ELOOP and other filesystem exceptions must become the reader's refusal class,
210
+ // not escape raw while an ordinary missing path is represented by undefined.
211
+ function statOrFsPathError(target, relativePath) {
212
+ try {
213
+ return statSync(target, { throwIfNoEntry: false });
214
+ }
215
+ catch (err) {
216
+ throw new FsFilePathError({ message: `path '${relativePath}' could not be read: ${err instanceof Error ? err.message : String(err)}` });
217
+ }
218
+ }
219
+ // A bounded leading-NUL heuristic keeps binary databases/images/archives out of inline UTF-8
220
+ // model input without making type sniffing proportional to the full file size.
221
+ function looksBinary(buffer) {
222
+ const sniffLength = Math.min(buffer.length, BINARY_SNIFF_BYTES);
223
+ for (let i = 0; i < sniffLength; i += 1) {
224
+ if (buffer[i] === 0)
225
+ return true;
226
+ }
227
+ return false;
228
+ }
229
+ function readFsFile(required) {
230
+ const target = resolveFsFilePath(required);
231
+ const stat = statOrFsPathError(target, required.relativePath);
232
+ if (!stat)
233
+ throw new FsFilePathError({ message: `file '${required.relativePath}' does not exist` });
234
+ if (!stat.isFile())
235
+ throw new FsFilePathError({ message: `path '${required.relativePath}' is not a regular file` });
236
+ if (stat.size > MAX_FS_FILE_BYTES) {
237
+ throw new FsFilePathError({ message: `file '${required.relativePath}' exceeds the ${MAX_FS_FILE_BYTES}-byte readable limit` });
238
+ }
239
+ const fd = fs.openSync(target, "r");
240
+ let buffer;
241
+ try {
242
+ const bounded = Buffer.alloc(MAX_FS_FILE_BYTES + 1);
243
+ let count = 0;
244
+ while (count < bounded.length) {
245
+ const read = fs.readSync(fd, bounded, count, bounded.length - count, null);
246
+ if (read === 0)
247
+ break;
248
+ count += read;
249
+ }
250
+ if (count > MAX_FS_FILE_BYTES)
251
+ throw new FsFilePathError({ message: `file '${required.relativePath}' exceeds the ${MAX_FS_FILE_BYTES}-byte readable limit` });
252
+ buffer = bounded.subarray(0, count);
253
+ }
254
+ finally {
255
+ fs.closeSync(fd);
256
+ }
257
+ if (looksBinary(buffer)) {
258
+ throw new FsFilePathError({ message: `file '${required.relativePath}' looks like a binary file and cannot be read as text` });
259
+ }
260
+ return { content: buffer.toString("utf8"), bytes: buffer.byteLength };
261
+ }
262
+ // A stat-only size check can be bypassed by a growing file: bound the stream independently and
263
+ // reject rather than return a clipped image/video. Destroy the stream on success or failure.
264
+ async function openFsFileForRead(required) {
265
+ const { relativePath, maxBytes } = required;
266
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
267
+ throw new FsFilePathError({ message: "maxBytes must be a positive safe integer" });
268
+ }
269
+ const target = resolveFsFilePath(required);
270
+ const stat = statOrFsPathError(target, relativePath);
271
+ if (!stat)
272
+ throw new FsFilePathError({ message: `file '${relativePath}' does not exist` });
273
+ if (!stat.isFile())
274
+ throw new FsFilePathError({ message: `path '${relativePath}' is not a regular file` });
275
+ if (stat.size > maxBytes) {
276
+ throw new FsFilePathError({ message: `file '${relativePath}' is ${stat.size} bytes and exceeds the ${maxBytes}-byte import limit` });
277
+ }
278
+ const stream = fs.createReadStream(target, { highWaterMark: Math.min(64 * 1024, maxBytes + 1) });
279
+ const chunks = [];
280
+ let totalBytes = 0;
281
+ try {
282
+ for await (const chunk of stream) {
283
+ const bytes = chunk;
284
+ totalBytes += bytes.byteLength;
285
+ if (totalBytes > maxBytes) {
286
+ throw new FsFilePathError({ message: `file '${relativePath}' is at least ${totalBytes} bytes and exceeds the ${maxBytes}-byte import limit` });
287
+ }
288
+ chunks.push(bytes);
289
+ }
290
+ return Buffer.concat(chunks, totalBytes);
291
+ }
292
+ catch (error) {
293
+ if (error instanceof FsFilePathError)
294
+ throw error;
295
+ throw new FsFilePathError({ message: `path '${relativePath}' could not be read: ${error instanceof Error ? error.message : String(error)}` });
296
+ }
297
+ finally {
298
+ stream.destroy();
299
+ }
300
+ }
301
+ return {
302
+ resolve: ({ relativePath }) => resolveFsFilePath({ rootPath, relativePath }),
303
+ read: ({ relativePath }) => readFsFile({ rootPath, relativePath }),
304
+ readBytes: ({ relativePath, maxBytes }) => openFsFileForRead({ rootPath, relativePath, maxBytes }),
305
+ list: (_required, optional = {}) => listFsFiles({ rootPath, ...optional }),
306
+ isDeniedFileName: ({ fileName }) => isDeniedFsFileName(fileName),
307
+ isDeniedPathSegment: ({ segmentName }) => isDeniedFsPathSegment(segmentName),
308
+ };
309
+ }
310
+ //# sourceMappingURL=guarded-reader.js.map
@@ -0,0 +1,8 @@
1
+ export * from "../fs.js";
2
+ export * from "./atomic-write.js";
3
+ export * from "./env-text.js";
4
+ export * from "./strict-containment.js";
5
+ export * from "./guarded-reader.js";
6
+ export * from "./durable-json.js";
7
+ export { withFileLock, withFileLockSync, isLockStale } from "./file-lock.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ // Filesystem exports share atomic replacement and token/inode-owned lock primitives.
2
+ export * from "../fs.js";
3
+ export * from "./atomic-write.js";
4
+ export * from "./env-text.js";
5
+ export * from "./strict-containment.js";
6
+ export * from "./guarded-reader.js";
7
+ export * from "./durable-json.js";
8
+ export { withFileLock, withFileLockSync, isLockStale } from "./file-lock.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,5 @@
1
+ export declare function resolvePathWithin({ root, segment }: {
2
+ root: string;
3
+ segment: string;
4
+ }): string | null;
5
+ //# sourceMappingURL=strict-containment.d.ts.map
@@ -0,0 +1,13 @@
1
+ /** Preserve strict lexical containment: relative roots, root equality and traversal are refused (except the filesystem-root edge case). Symlinks require a separate physical-path guard. */
2
+ import path from "node:path";
3
+ export function resolvePathWithin({ root, segment }) {
4
+ const resolved = path.resolve(root, segment);
5
+ if (resolved !== path.join(root, segment))
6
+ return null;
7
+ const normalizedRoot = path.resolve(root);
8
+ const prefix = normalizedRoot === path.sep ? normalizedRoot : `${normalizedRoot}${path.sep}`;
9
+ if (!resolved.startsWith(prefix))
10
+ return null;
11
+ return resolved;
12
+ }
13
+ //# sourceMappingURL=strict-containment.js.map
package/dist/fs.js CHANGED
@@ -75,9 +75,8 @@ export async function atomicCopyFile(sourcePath, destinationPath, options = {})
75
75
  const tempPath = join(destinationDir, `.${basename(destination)}.${process.pid}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}.tmp`);
76
76
  try {
77
77
  await copyFile(source, tempPath);
78
- if (options.overwrite === true) {
79
- await rm(destination, { force: true });
80
- }
78
+ // Rename replaces the old file atomically. Removing it first opens a
79
+ // missing-file window and loses the previous contents if rename fails.
81
80
  await rename(tempPath, destination);
82
81
  const copied = await stat(destination);
83
82
  return { bytesCopied: copied.size, replaced: existing != null };
@@ -0,0 +1,13 @@
1
+ import type { HttpClientPort } from "@jini-ai/core/primitives";
2
+ import type { GuardedHttpRequired } from "./ports.js";
3
+ export type AddressClass = "public" | "private" | "loopback" | "link-local" | "reserved";
4
+ /** Uses the canonical net address validator; labels preserve the guarded refusal contract. */
5
+ export declare function classifyAddress({ ip }: {
6
+ ip: string;
7
+ }): AddressClass;
8
+ /** Builds an adapter-friendly outbound port. Every hop is resolved, checked, and pinned before I/O.
9
+ * Socket idle time is bounded on every hop. An optional totalDeadlineMs includes DNS,
10
+ * redirect hops and decoded body reads; it is never inferred from the idle budget.
11
+ */
12
+ export declare function createHttpClient(required: GuardedHttpRequired): HttpClientPort;
13
+ //# sourceMappingURL=client.d.ts.map