@nifrajs/storage 1.4.0 → 1.6.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.
package/dist/file.d.ts CHANGED
@@ -6,6 +6,11 @@ export declare class FileStorage implements StorageAdapter {
6
6
  private metadataPathFor;
7
7
  /** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
8
8
  private pathFor;
9
+ private symlinkError;
10
+ /** Reject existing symbolic links below `base`; missing suffixes are safe for later creation. */
11
+ private assertNoSymlinkPath;
12
+ /** Create parents, re-check them, and refuse a final-component symlink at open time. */
13
+ private writeContained;
9
14
  put(key: string, data: StorageData, options?: PutOptions): Promise<void>;
10
15
  get(key: string): Promise<StorageObject | null>;
11
16
  delete(key: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAA;AA4BnB,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,IAAI,EAAE,MAAM;IAOxB,OAAO,CAAC,eAAe;IAWvB,wHAAwH;IACxH,OAAO,CAAC,OAAO;IAST,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAuB/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUrC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAU1C,IAAI;CAgBnB"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAA;AA4BnB,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,IAAI,EAAE,MAAM;IAOxB,OAAO,CAAC,eAAe;IAWvB,wHAAwH;IACxH,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,YAAY;IAMpB,iGAAiG;YACnF,mBAAmB;IAsBjC,wFAAwF;YAC1E,cAAc;IA0BtB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IA+B/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYrC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAU1C,IAAI;CAiBnB"}
package/dist/file.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
5
5
  * objects created before sidecar support still infer content type from their extension.
6
6
  */
7
- import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
7
+ import { constants } from "node:fs";
8
+ import { lstat, mkdir, open, readdir, readFile, rm, stat } from "node:fs/promises";
8
9
  import { dirname, join, posix, resolve, sep } from "node:path";
9
10
  import { assertSafeKey, StorageKeyError } from "./key.js";
10
11
  import { toBytes, } from "./types.js";
@@ -58,36 +59,98 @@ export class FileStorage {
58
59
  }
59
60
  return full;
60
61
  }
62
+ symlinkError(key) {
63
+ return new StorageKeyError(`storage key ${JSON.stringify(key)} crosses a symbolic link beneath the storage root`);
64
+ }
65
+ /** Reject existing symbolic links below `base`; missing suffixes are safe for later creation. */
66
+ async assertNoSymlinkPath(base, path, key) {
67
+ try {
68
+ if ((await lstat(base)).isSymbolicLink())
69
+ throw this.symlinkError(key);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof StorageKeyError)
73
+ throw error;
74
+ if (error.code === "ENOENT")
75
+ return;
76
+ throw error;
77
+ }
78
+ const suffix = path.slice(base.length + sep.length);
79
+ let current = base;
80
+ for (const segment of suffix.split(sep)) {
81
+ current = join(current, segment);
82
+ try {
83
+ if ((await lstat(current)).isSymbolicLink())
84
+ throw this.symlinkError(key);
85
+ }
86
+ catch (error) {
87
+ if (error instanceof StorageKeyError)
88
+ throw error;
89
+ if (error.code === "ENOENT")
90
+ return;
91
+ throw error;
92
+ }
93
+ }
94
+ }
95
+ /** Create parents, re-check them, and refuse a final-component symlink at open time. */
96
+ async writeContained(base, path, key, data) {
97
+ await this.assertNoSymlinkPath(base, path, key);
98
+ await mkdir(dirname(path), { recursive: true });
99
+ await this.assertNoSymlinkPath(base, path, key);
100
+ try {
101
+ const handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o666);
102
+ try {
103
+ await handle.writeFile(data);
104
+ }
105
+ finally {
106
+ await handle.close();
107
+ }
108
+ }
109
+ catch (error) {
110
+ if (error.code === "ELOOP")
111
+ throw this.symlinkError(key);
112
+ throw error;
113
+ }
114
+ }
61
115
  async put(key, data, options = {}) {
62
116
  const path = this.pathFor(key);
63
- await mkdir(dirname(path), { recursive: true });
64
- await writeFile(path, toBytes(data));
117
+ await this.writeContained(this.root, path, key, toBytes(data));
65
118
  const metadataPath = this.metadataPathFor(key);
66
119
  if (options.contentType !== undefined || options.metadata !== undefined) {
67
- await mkdir(dirname(metadataPath), { recursive: true });
68
- await writeFile(metadataPath, JSON.stringify({
120
+ await this.writeContained(this.metadataRoot, metadataPath, key, JSON.stringify({
69
121
  ...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
70
122
  ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
71
123
  }));
72
124
  }
73
125
  else {
126
+ await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
74
127
  await rm(metadataPath, { force: true });
75
128
  }
76
129
  }
77
130
  async get(key) {
78
131
  const path = this.pathFor(key);
132
+ await this.assertNoSymlinkPath(this.root, path, key);
79
133
  let body;
80
134
  try {
81
135
  body = new Uint8Array(await readFile(path));
82
136
  }
83
- catch {
84
- return null; // ENOENT (or unreadable) → treated as missing
137
+ catch (error) {
138
+ if (error.code === "ENOENT")
139
+ return null;
140
+ throw error;
85
141
  }
86
142
  let stored = {};
87
143
  try {
88
- stored = JSON.parse(await readFile(this.metadataPathFor(key), "utf8"));
144
+ const metadataPath = this.metadataPathFor(key);
145
+ await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
146
+ stored = JSON.parse(await readFile(metadataPath, "utf8"));
89
147
  }
90
- catch {
148
+ catch (error) {
149
+ if (error instanceof StorageKeyError)
150
+ throw error;
151
+ if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) {
152
+ throw error;
153
+ }
91
154
  // Objects created before sidecar support still infer a useful MIME type from their extension.
92
155
  }
93
156
  const contentType = stored.contentType ?? inferContentType(key);
@@ -99,19 +162,25 @@ export class FileStorage {
99
162
  };
100
163
  }
101
164
  async delete(key) {
165
+ const path = this.pathFor(key);
166
+ const metadataPath = this.metadataPathFor(key);
102
167
  await Promise.all([
103
- rm(this.pathFor(key), { force: true }),
104
- rm(this.metadataPathFor(key), { force: true }),
168
+ this.assertNoSymlinkPath(this.root, path, key),
169
+ this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
105
170
  ]);
171
+ await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })]);
106
172
  }
107
173
  async exists(key) {
108
174
  const path = this.pathFor(key);
175
+ await this.assertNoSymlinkPath(this.root, path, key);
109
176
  try {
110
177
  await stat(path);
111
178
  return true;
112
179
  }
113
- catch {
114
- return false;
180
+ catch (error) {
181
+ if (error.code === "ENOENT")
182
+ return false;
183
+ throw error;
115
184
  }
116
185
  }
117
186
  async list(options = {}) {
@@ -136,7 +205,9 @@ export class FileStorage {
136
205
  const full = join(dir, name);
137
206
  // POSIX-join the key segments so listed keys are portable regardless of the host separator.
138
207
  const key = prefix === "" ? name : posix.join(prefix, name);
139
- const info = await stat(full);
208
+ const info = await lstat(full);
209
+ if (info.isSymbolicLink())
210
+ continue;
140
211
  if (info.isDirectory())
141
212
  await this.walk(full, key, out);
142
213
  else if (info.isFile())
package/dist/file.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/storage",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Blob storage mechanics for nifra — traversal-safe memory, filesystem, and R2 adapters, optional provider capabilities, and executable adapter conformance.",
5
5
  "keywords": [
6
6
  "bun",
package/src/file.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  * traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
5
5
  * objects created before sidecar support still infer content type from their extension.
6
6
  */
7
- import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"
7
+ import { constants } from "node:fs"
8
+ import { lstat, mkdir, open, readdir, readFile, rm, stat } from "node:fs/promises"
8
9
  import { dirname, join, posix, resolve, sep } from "node:path"
9
10
  import { assertSafeKey, StorageKeyError } from "./key.ts"
10
11
  import {
@@ -74,37 +75,102 @@ export class FileStorage implements StorageAdapter {
74
75
  return full
75
76
  }
76
77
 
78
+ private symlinkError(key: string): StorageKeyError {
79
+ return new StorageKeyError(
80
+ `storage key ${JSON.stringify(key)} crosses a symbolic link beneath the storage root`,
81
+ )
82
+ }
83
+
84
+ /** Reject existing symbolic links below `base`; missing suffixes are safe for later creation. */
85
+ private async assertNoSymlinkPath(base: string, path: string, key: string): Promise<void> {
86
+ try {
87
+ if ((await lstat(base)).isSymbolicLink()) throw this.symlinkError(key)
88
+ } catch (error) {
89
+ if (error instanceof StorageKeyError) throw error
90
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return
91
+ throw error
92
+ }
93
+ const suffix = path.slice(base.length + sep.length)
94
+ let current = base
95
+ for (const segment of suffix.split(sep)) {
96
+ current = join(current, segment)
97
+ try {
98
+ if ((await lstat(current)).isSymbolicLink()) throw this.symlinkError(key)
99
+ } catch (error) {
100
+ if (error instanceof StorageKeyError) throw error
101
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return
102
+ throw error
103
+ }
104
+ }
105
+ }
106
+
107
+ /** Create parents, re-check them, and refuse a final-component symlink at open time. */
108
+ private async writeContained(
109
+ base: string,
110
+ path: string,
111
+ key: string,
112
+ data: Uint8Array | string,
113
+ ): Promise<void> {
114
+ await this.assertNoSymlinkPath(base, path, key)
115
+ await mkdir(dirname(path), { recursive: true })
116
+ await this.assertNoSymlinkPath(base, path, key)
117
+ try {
118
+ const handle = await open(
119
+ path,
120
+ constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW,
121
+ 0o666,
122
+ )
123
+ try {
124
+ await handle.writeFile(data)
125
+ } finally {
126
+ await handle.close()
127
+ }
128
+ } catch (error) {
129
+ if ((error as NodeJS.ErrnoException).code === "ELOOP") throw this.symlinkError(key)
130
+ throw error
131
+ }
132
+ }
133
+
77
134
  async put(key: string, data: StorageData, options: PutOptions = {}): Promise<void> {
78
135
  const path = this.pathFor(key)
79
- await mkdir(dirname(path), { recursive: true })
80
- await writeFile(path, toBytes(data))
136
+ await this.writeContained(this.root, path, key, toBytes(data))
81
137
  const metadataPath = this.metadataPathFor(key)
82
138
  if (options.contentType !== undefined || options.metadata !== undefined) {
83
- await mkdir(dirname(metadataPath), { recursive: true })
84
- await writeFile(
139
+ await this.writeContained(
140
+ this.metadataRoot,
85
141
  metadataPath,
142
+ key,
86
143
  JSON.stringify({
87
144
  ...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
88
145
  ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
89
146
  }),
90
147
  )
91
148
  } else {
149
+ await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key)
92
150
  await rm(metadataPath, { force: true })
93
151
  }
94
152
  }
95
153
 
96
154
  async get(key: string): Promise<StorageObject | null> {
97
155
  const path = this.pathFor(key)
156
+ await this.assertNoSymlinkPath(this.root, path, key)
98
157
  let body: Uint8Array
99
158
  try {
100
159
  body = new Uint8Array(await readFile(path))
101
- } catch {
102
- return null // ENOENT (or unreadable) → treated as missing
160
+ } catch (error) {
161
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null
162
+ throw error
103
163
  }
104
164
  let stored: { contentType?: string; metadata?: Readonly<Record<string, string>> } = {}
105
165
  try {
106
- stored = JSON.parse(await readFile(this.metadataPathFor(key), "utf8")) as typeof stored
107
- } catch {
166
+ const metadataPath = this.metadataPathFor(key)
167
+ await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key)
168
+ stored = JSON.parse(await readFile(metadataPath, "utf8")) as typeof stored
169
+ } catch (error) {
170
+ if (error instanceof StorageKeyError) throw error
171
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !(error instanceof SyntaxError)) {
172
+ throw error
173
+ }
108
174
  // Objects created before sidecar support still infer a useful MIME type from their extension.
109
175
  }
110
176
  const contentType = stored.contentType ?? inferContentType(key)
@@ -117,19 +183,24 @@ export class FileStorage implements StorageAdapter {
117
183
  }
118
184
 
119
185
  async delete(key: string): Promise<void> {
186
+ const path = this.pathFor(key)
187
+ const metadataPath = this.metadataPathFor(key)
120
188
  await Promise.all([
121
- rm(this.pathFor(key), { force: true }),
122
- rm(this.metadataPathFor(key), { force: true }),
189
+ this.assertNoSymlinkPath(this.root, path, key),
190
+ this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
123
191
  ])
192
+ await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })])
124
193
  }
125
194
 
126
195
  async exists(key: string): Promise<boolean> {
127
196
  const path = this.pathFor(key)
197
+ await this.assertNoSymlinkPath(this.root, path, key)
128
198
  try {
129
199
  await stat(path)
130
200
  return true
131
- } catch {
132
- return false
201
+ } catch (error) {
202
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false
203
+ throw error
133
204
  }
134
205
  }
135
206
 
@@ -154,7 +225,8 @@ export class FileStorage implements StorageAdapter {
154
225
  const full = join(dir, name)
155
226
  // POSIX-join the key segments so listed keys are portable regardless of the host separator.
156
227
  const key = prefix === "" ? name : posix.join(prefix, name)
157
- const info = await stat(full)
228
+ const info = await lstat(full)
229
+ if (info.isSymbolicLink()) continue
158
230
  if (info.isDirectory()) await this.walk(full, key, out)
159
231
  else if (info.isFile()) out.push(key)
160
232
  }