@remnic/core 9.3.519 → 9.3.521

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.519",
3
+ "version": "9.3.521",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -6,7 +6,7 @@
6
6
  * so swapping storage providers requires no pipeline changes.
7
7
  */
8
8
 
9
- import fs from "node:fs";
9
+ import type { Stats } from "node:fs";
10
10
  import fsp from "node:fs/promises";
11
11
  import path from "node:path";
12
12
  import type { BinaryStorageBackendConfig } from "./types.js";
@@ -27,6 +27,8 @@ export interface BinaryStorageBackend {
27
27
  exists(remotePath: string): Promise<boolean>;
28
28
  /** Delete a file from the backend. */
29
29
  delete(remotePath: string): Promise<void>;
30
+ /** Return the user-resolvable markdown target for a stored backend path. */
31
+ getRedirectTarget?(remotePath: string): string;
30
32
  }
31
33
 
32
34
  // ---------------------------------------------------------------------------
@@ -45,10 +47,9 @@ export class FilesystemBackend implements BinaryStorageBackend {
45
47
  }
46
48
 
47
49
  private resolveRemotePath(remotePath: string): string {
48
- if (path.isAbsolute(remotePath)) {
49
- throw new Error(`FilesystemBackend remotePath must be relative: ${JSON.stringify(remotePath)}`);
50
- }
51
- const resolved = path.resolve(this.basePath, remotePath);
50
+ const resolved = path.isAbsolute(remotePath)
51
+ ? path.resolve(remotePath)
52
+ : path.resolve(this.basePath, remotePath);
52
53
  const relative = path.relative(this.basePath, resolved);
53
54
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
54
55
  throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
@@ -56,26 +57,169 @@ export class FilesystemBackend implements BinaryStorageBackend {
56
57
  return resolved;
57
58
  }
58
59
 
59
- async upload(localPath: string, remotePath: string): Promise<string> {
60
+ private isInsideBase(candidate: string, realBase: string): boolean {
61
+ const relative = path.relative(realBase, candidate);
62
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
63
+ }
64
+
65
+ private async realBasePathIfExists(): Promise<string | null> {
66
+ try {
67
+ const stat = await fsp.lstat(this.basePath);
68
+ if (stat.isSymbolicLink()) {
69
+ throw new Error(`FilesystemBackend basePath must not be a symlink: ${this.basePath}`);
70
+ }
71
+ if (!stat.isDirectory()) {
72
+ throw new Error(`FilesystemBackend basePath must be a directory: ${this.basePath}`);
73
+ }
74
+ return await fsp.realpath(this.basePath);
75
+ } catch (err) {
76
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
77
+ return null;
78
+ }
79
+ throw err;
80
+ }
81
+ }
82
+
83
+ private async ensureBaseDirectory(): Promise<string> {
84
+ await fsp.mkdir(this.basePath, { recursive: true });
85
+ const realBase = await this.realBasePathIfExists();
86
+ if (realBase === null) {
87
+ throw new Error(`FilesystemBackend failed to create basePath: ${this.basePath}`);
88
+ }
89
+ return realBase;
90
+ }
91
+
92
+ private async ensureSafeParentDirectory(dest: string): Promise<string> {
93
+ const realBase = await this.ensureBaseDirectory();
94
+ const destDir = path.dirname(dest);
95
+ const relativeDir = path.relative(this.basePath, destDir);
96
+ const segments = relativeDir === "" ? [] : relativeDir.split(path.sep);
97
+ let current = this.basePath;
98
+
99
+ for (const segment of segments) {
100
+ if (segment === "." || segment === "") continue;
101
+ current = path.join(current, segment);
102
+ try {
103
+ const stat = await fsp.lstat(current);
104
+ if (stat.isSymbolicLink()) {
105
+ throw new Error(`FilesystemBackend remotePath traverses symlink: ${current}`);
106
+ }
107
+ if (!stat.isDirectory()) {
108
+ throw new Error(`FilesystemBackend remotePath parent is not a directory: ${current}`);
109
+ }
110
+ } catch (err) {
111
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
112
+ throw err;
113
+ }
114
+ await fsp.mkdir(current);
115
+ }
116
+ }
117
+
118
+ const realParent = await fsp.realpath(destDir);
119
+ if (!this.isInsideBase(realParent, realBase)) {
120
+ throw new Error(`FilesystemBackend remotePath parent escapes basePath: ${dest}`);
121
+ }
122
+ return realBase;
123
+ }
124
+
125
+ private async resolveExistingRemotePath(remotePath: string): Promise<string | null> {
60
126
  const dest = this.resolveRemotePath(remotePath);
127
+ const realBase = await this.realBasePathIfExists();
128
+ if (realBase === null) {
129
+ return null;
130
+ }
131
+
61
132
  const destDir = path.dirname(dest);
62
- await fsp.mkdir(destDir, { recursive: true });
63
- await fsp.copyFile(localPath, dest);
133
+ const relativeDir = path.relative(this.basePath, destDir);
134
+ const segments = relativeDir === "" ? [] : relativeDir.split(path.sep);
135
+ let current = this.basePath;
136
+ for (const segment of segments) {
137
+ if (segment === "." || segment === "") continue;
138
+ current = path.join(current, segment);
139
+ let stat: Stats;
140
+ try {
141
+ stat = await fsp.lstat(current);
142
+ } catch (err) {
143
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
144
+ return null;
145
+ }
146
+ throw err;
147
+ }
148
+ if (stat.isSymbolicLink()) {
149
+ throw new Error(`FilesystemBackend remotePath traverses symlink: ${current}`);
150
+ }
151
+ if (!stat.isDirectory()) {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ const realParent = await fsp.realpath(destDir).catch((err: NodeJS.ErrnoException) => {
157
+ if (err.code === "ENOENT") return null;
158
+ throw err;
159
+ });
160
+ if (realParent === null) {
161
+ return null;
162
+ }
163
+ if (!this.isInsideBase(realParent, realBase)) {
164
+ throw new Error(`FilesystemBackend remotePath parent escapes basePath: ${JSON.stringify(remotePath)}`);
165
+ }
166
+
167
+ try {
168
+ const stat = await fsp.lstat(dest);
169
+ if (stat.isSymbolicLink()) {
170
+ throw new Error(`FilesystemBackend remotePath points to symlink: ${dest}`);
171
+ }
172
+ if (!stat.isFile()) {
173
+ return null;
174
+ }
175
+ } catch (err) {
176
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
177
+ return null;
178
+ }
179
+ throw err;
180
+ }
181
+
182
+ const realDest = await fsp.realpath(dest);
183
+ if (!this.isInsideBase(realDest, realBase)) {
184
+ throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
185
+ }
64
186
  return dest;
65
187
  }
66
188
 
67
- async exists(remotePath: string): Promise<boolean> {
189
+ async upload(localPath: string, remotePath: string): Promise<string> {
190
+ if (path.isAbsolute(remotePath)) {
191
+ throw new Error(`FilesystemBackend upload remotePath must be relative: ${JSON.stringify(remotePath)}`);
192
+ }
68
193
  const dest = this.resolveRemotePath(remotePath);
194
+ const realBase = await this.ensureSafeParentDirectory(dest);
69
195
  try {
70
- await fsp.access(dest, fs.constants.F_OK);
71
- return true;
72
- } catch {
73
- return false;
196
+ const stat = await fsp.lstat(dest);
197
+ if (stat.isSymbolicLink()) {
198
+ throw new Error(`FilesystemBackend remotePath points to symlink: ${dest}`);
199
+ }
200
+ } catch (err) {
201
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
202
+ throw err;
203
+ }
74
204
  }
205
+ await fsp.copyFile(localPath, dest);
206
+ const realDest = await fsp.realpath(dest);
207
+ if (!this.isInsideBase(realDest, realBase)) {
208
+ throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
209
+ }
210
+ return remotePath;
211
+ }
212
+
213
+ async exists(remotePath: string): Promise<boolean> {
214
+ const dest = await this.resolveExistingRemotePath(remotePath);
215
+ return dest !== null;
75
216
  }
76
217
 
77
218
  async delete(remotePath: string): Promise<void> {
78
- const dest = this.resolveRemotePath(remotePath);
219
+ const dest = await this.resolveExistingRemotePath(remotePath);
220
+ if (dest === null) {
221
+ return;
222
+ }
79
223
  try {
80
224
  await fsp.unlink(dest);
81
225
  } catch (err: unknown) {
@@ -83,6 +227,10 @@ export class FilesystemBackend implements BinaryStorageBackend {
83
227
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
84
228
  }
85
229
  }
230
+
231
+ getRedirectTarget(remotePath: string): string {
232
+ return this.resolveRemotePath(remotePath);
233
+ }
86
234
  }
87
235
 
88
236
  // ---------------------------------------------------------------------------
@@ -24,25 +24,37 @@ export function manifestPath(memoryDir: string): string {
24
24
 
25
25
  /**
26
26
  * Read the manifest from disk. Returns a fresh empty manifest if the file
27
- * does not exist or contains invalid JSON (CLAUDE.md #18).
27
+ * does not exist. Existing invalid manifests fail closed so the pipeline does
28
+ * not overwrite state needed for safe cleanup.
28
29
  */
29
30
  export async function readManifest(memoryDir: string): Promise<BinaryLifecycleManifest> {
30
31
  const filePath = manifestPath(memoryDir);
32
+ let raw: string;
31
33
  try {
32
- const raw = await fsp.readFile(filePath, "utf-8");
33
- const parsed: unknown = JSON.parse(raw);
34
- // CLAUDE.md #18: validate the parsed result is a non-null object.
35
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
34
+ raw = await fsp.readFile(filePath, "utf-8");
35
+ } catch (err) {
36
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
36
37
  return emptyManifest();
37
38
  }
38
- const obj = parsed as Record<string, unknown>;
39
- if (obj.version !== 1 || !Array.isArray(obj.assets)) {
40
- return emptyManifest();
41
- }
42
- return parsed as BinaryLifecycleManifest;
43
- } catch {
44
- return emptyManifest();
39
+ throw new Error(`Failed to read binary lifecycle manifest at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
40
+ }
41
+
42
+ let parsed: unknown;
43
+ try {
44
+ parsed = JSON.parse(raw);
45
+ } catch (err) {
46
+ throw new Error(`Invalid binary lifecycle manifest JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
47
+ }
48
+
49
+ // CLAUDE.md #18: validate the parsed result is a non-null object.
50
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
51
+ throw new Error(`Invalid binary lifecycle manifest shape at ${filePath}: expected object`);
52
+ }
53
+ const obj = parsed as Record<string, unknown>;
54
+ if (obj.version !== 1 || !Array.isArray(obj.assets)) {
55
+ throw new Error(`Invalid binary lifecycle manifest shape at ${filePath}: expected version 1 with assets array`);
45
56
  }
57
+ return parsed as BinaryLifecycleManifest;
46
58
  }
47
59
 
48
60
  /**