@porulle/adapter-local-storage 0.10.3 → 0.10.7

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/README.md CHANGED
@@ -39,7 +39,7 @@ Every key passes through `resolveSafePath()` which:
39
39
  - Verifies the result stays inside `basePath`
40
40
  - Rejects `..`, NUL bytes, absolute paths, and any input that would escape the storage root
41
41
 
42
- This closed a CVE-class arbitrary-file-write surface (a key like `../../../../etc/passwd` would otherwise write outside the storage root). See `packages/core/test/` for the regression tests.
42
+ This closed a CVE-class arbitrary-file-write surface (a key that walks up out of the root would otherwise write outside it). See `packages/core/test/` for the regression tests.
43
43
 
44
44
  ## What it implements
45
45
 
@@ -47,5 +47,5 @@ This closed a CVE-class arbitrary-file-write surface (a key like `../../../../et
47
47
 
48
48
  ## See also
49
49
 
50
- - [`SECURITY.md`](../../../SECURITY.md) — storage hardening
50
+ - [`SECURITY.md`](https://github.com/asyncdotengineering/porulle/blob/main/SECURITY.md) — storage hardening
51
51
  - `@porulle/adapter-s3`, `@porulle/adapter-r2` — production storage adapters
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAS1E,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiCD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,cAAc,CA6EvF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAS1E,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiCD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,cAAc,CAiFvF"}
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve, sep } from "node:path";
3
3
  import { Err, Ok } from "@porulle/core";
4
4
  /**
5
5
  * Resolves a key against basePath and verifies the result stays inside
6
- * basePath. Rejects path-traversal payloads like `../../etc/passwd`,
7
- * absolute paths, and any other input that would escape the storage root.
6
+ * basePath. Rejects parent-directory traversal, absolute paths, and any
7
+ * other input that would escape the storage root.
8
8
  *
9
- * Without this guard an upload with key `../../something` would write
10
- * outside the storage root (CVE-class: arbitrary file write).
9
+ * Without this guard an upload whose key walks up out of the root would
10
+ * write outside it (CVE-class: arbitrary file write).
11
11
  */
12
12
  function resolveSafePath(basePath, key) {
13
13
  if (typeof key !== "string" || key.length === 0) {
@@ -67,7 +67,14 @@ export function localStorageAdapter(options) {
67
67
  },
68
68
  async delete(key) {
69
69
  const path = resolveSafePath(options.basePath, key);
70
- await rm(path, { force: true });
70
+ try {
71
+ await unlink(path);
72
+ }
73
+ catch (error) {
74
+ // Deleting an absent key stays a no-op, as it was under rm({ force: true }).
75
+ if (error.code !== "ENOENT")
76
+ throw error;
77
+ }
71
78
  return Ok(undefined);
72
79
  },
73
80
  async list(prefix) {
@@ -80,12 +87,11 @@ export function localStorageAdapter(options) {
80
87
  const info = await stat(filePath);
81
88
  if (info.isDirectory())
82
89
  continue;
83
- const content = await readFile(filePath);
84
90
  output.push({
85
91
  key: file,
86
92
  url: `${baseUrl}/${prefix}/${file}`,
87
93
  contentType: "application/octet-stream",
88
- size: content.byteLength,
94
+ size: info.size,
89
95
  });
90
96
  }
91
97
  return Ok(output);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/adapter-local-storage",
3
- "version": "0.10.3",
3
+ "version": "0.10.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,15 +11,15 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@porulle/core": "0.10.3"
14
+ "@porulle/core": "0.10.8"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/node": "^24.5.2",
18
18
  "eslint": "^9.39.1",
19
19
  "typescript": "5.9.2",
20
20
  "vitest": "^3.2.4",
21
- "@porulle/eslint-config": "0.1.0",
22
- "@porulle/typescript-config": "0.1.0"
21
+ "@porulle/typescript-config": "0.1.0",
22
+ "@porulle/eslint-config": "0.1.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve, sep } from "node:path";
3
3
  import { Err, Ok, type Result, type StorageAdapter } from "@porulle/core";
4
4
 
@@ -16,11 +16,11 @@ export interface LocalStorageAdapterOptions {
16
16
 
17
17
  /**
18
18
  * Resolves a key against basePath and verifies the result stays inside
19
- * basePath. Rejects path-traversal payloads like `../../etc/passwd`,
20
- * absolute paths, and any other input that would escape the storage root.
19
+ * basePath. Rejects parent-directory traversal, absolute paths, and any
20
+ * other input that would escape the storage root.
21
21
  *
22
- * Without this guard an upload with key `../../something` would write
23
- * outside the storage root (CVE-class: arbitrary file write).
22
+ * Without this guard an upload whose key walks up out of the root would
23
+ * write outside it (CVE-class: arbitrary file write).
24
24
  */
25
25
  function resolveSafePath(basePath: string, key: string): string {
26
26
  if (typeof key !== "string" || key.length === 0) {
@@ -89,7 +89,12 @@ export function localStorageAdapter(options: LocalStorageAdapterOptions): Storag
89
89
 
90
90
  async delete(key: string): Promise<Result<void>> {
91
91
  const path = resolveSafePath(options.basePath, key);
92
- await rm(path, { force: true });
92
+ try {
93
+ await unlink(path);
94
+ } catch (error) {
95
+ // Deleting an absent key stays a no-op, as it was under rm({ force: true }).
96
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
97
+ }
93
98
  return Ok(undefined);
94
99
  },
95
100
 
@@ -104,12 +109,11 @@ export function localStorageAdapter(options: LocalStorageAdapterOptions): Storag
104
109
  const info = await stat(filePath);
105
110
  if (info.isDirectory()) continue;
106
111
 
107
- const content = await readFile(filePath);
108
112
  output.push({
109
113
  key: file,
110
114
  url: `${baseUrl}/${prefix}/${file}`,
111
115
  contentType: "application/octet-stream",
112
- size: content.byteLength,
116
+ size: info.size,
113
117
  });
114
118
  }
115
119