@nifrajs/storage 1.3.1 → 1.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.
package/README.md CHANGED
@@ -20,12 +20,25 @@ const keys = await storage.list({ prefix: "avatars/" })
20
20
  | Adapter | Use | Notes |
21
21
  |---|---|---|
22
22
  | `MemoryStorage` | dev / tests | not durable, not shared across instances |
23
- | `FileStorage(root)` | a long-running server (Bun/Node/Deno) | bytes-only: `contentType` inferred from the extension, custom metadata not persisted |
23
+ | `FileStorage(root)` | a long-running server (Bun/Node/Deno) | persists content type + custom metadata in an adjacent sidecar tree; legacy objects still infer MIME from extension |
24
24
  | `R2Storage(env.BUCKET)` | the edge (Cloudflare R2) | round-trips `contentType` + metadata; binding typed structurally (no `@cloudflare/workers-types`) |
25
25
 
26
26
  Implement `StorageAdapter` (`put` / `get` / `delete` / `exists` / `list`) for **S3 / GCS / anything else** —
27
27
  the same five methods, and your routes don't change.
28
28
 
29
+ Run the executable contract against third-party adapters:
30
+
31
+ ```ts
32
+ import { assertStorageAdapterConformance } from "@nifrajs/storage"
33
+
34
+ await assertStorageAdapterConformance({ createAdapter: () => new MyStorage() })
35
+ ```
36
+
37
+ Provider-specific mechanics stay optional: implement `PagedStorageAdapter`, `PresignableStorageAdapter`,
38
+ and/or `MovableStorageAdapter` only when the provider supports cursor listing, URL signing, or server-side
39
+ copy/move. Asset sensitivity, bucket routing, credentials, and TTL policy belong in the consuming app or
40
+ private package—not in these mechanical interfaces.
41
+
29
42
  ## Key safety
30
43
 
31
44
  Keys are POSIX-ish relative paths (`avatars/u1.png`). **Every** adapter rejects unsafe keys — absolute
@@ -42,3 +55,5 @@ import { assertSafeKey, StorageKeyError } from "@nifrajs/storage"
42
55
  - `put(key, data, { contentType?, metadata? })` — `data` is `Uint8Array | ArrayBuffer | string`.
43
56
  - `get(key)` → `{ body, size, contentType?, metadata? } | null` · `delete(key)` · `exists(key)` · `list({ prefix?, limit? })`.
44
57
  - `assertSafeKey(key)` / `StorageKeyError` · `toBytes(data)`.
58
+ - `assertStorageAdapterConformance({ createAdapter })` — dependency-free executable adapter contract.
59
+ - Optional interfaces: `PagedStorageAdapter` · `PresignableStorageAdapter` · `MovableStorageAdapter`.
@@ -0,0 +1,23 @@
1
+ import type { StorageAdapter } from "./types.js";
2
+ /** Construction and cleanup hooks for {@link assertStorageAdapterConformance}. */
3
+ export interface StorageAdapterConformanceOptions {
4
+ /** Return an empty, isolated adapter for this run. */
5
+ readonly createAdapter: () => StorageAdapter | Promise<StorageAdapter>;
6
+ }
7
+ /** A failed invariant reported by {@link assertStorageAdapterConformance}. */
8
+ export declare class StorageAdapterConformanceError extends Error {
9
+ readonly check: string;
10
+ constructor(check: string, message: string, cause?: unknown);
11
+ }
12
+ /**
13
+ * Execute the observable {@link StorageAdapter} contract without depending on a test runner.
14
+ *
15
+ * The adapter must be isolated because this writes deterministic conformance keys. The promise resolves
16
+ * only after proving byte + metadata round-trips, overwrite, missing/delete semantics, prefix/limit listing,
17
+ * and uniform unsafe-key rejection across every key-taking method. When the adapter also implements an
18
+ * optional capability ({@link PagedStorageAdapter}, {@link PresignableStorageAdapter},
19
+ * {@link MovableStorageAdapter}), that capability's contract is proven too — implementing a capability
20
+ * partially or unsafely fails conformance rather than silently escaping it.
21
+ */
22
+ export declare function assertStorageAdapterConformance(options: StorageAdapterConformanceOptions): Promise<void>;
23
+ //# sourceMappingURL=conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,cAAc,EACf,MAAM,YAAY,CAAA;AAEnB,kFAAkF;AAClF,MAAM,WAAW,gCAAgC;IAC/C,sDAAsD;IACtD,QAAQ,CAAC,aAAa,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CACvE;AAED,8EAA8E;AAC9E,qBAAa,8BAA+B,SAAQ,KAAK;IAErD,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO;CAKlB;AAkBD;;;;;;;;;GASG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,IAAI,CAAC,CA4Jf"}
@@ -0,0 +1,178 @@
1
+ /** A failed invariant reported by {@link assertStorageAdapterConformance}. */
2
+ export class StorageAdapterConformanceError extends Error {
3
+ check;
4
+ constructor(check, message, cause) {
5
+ super(`StorageAdapter conformance failed (${check}): ${message}`, { cause });
6
+ this.check = check;
7
+ this.name = "StorageAdapterConformanceError";
8
+ }
9
+ }
10
+ const fail = (check, message, cause) => {
11
+ throw new StorageAdapterConformanceError(check, message, cause);
12
+ };
13
+ const decode = (bytes) => new TextDecoder().decode(bytes);
14
+ const hasPaging = (storage) => typeof storage.listPage === "function";
15
+ const hasPresign = (storage) => typeof storage.presign === "function";
16
+ const hasMove = (storage) => typeof storage.copy === "function" &&
17
+ typeof storage.move === "function";
18
+ /**
19
+ * Execute the observable {@link StorageAdapter} contract without depending on a test runner.
20
+ *
21
+ * The adapter must be isolated because this writes deterministic conformance keys. The promise resolves
22
+ * only after proving byte + metadata round-trips, overwrite, missing/delete semantics, prefix/limit listing,
23
+ * and uniform unsafe-key rejection across every key-taking method. When the adapter also implements an
24
+ * optional capability ({@link PagedStorageAdapter}, {@link PresignableStorageAdapter},
25
+ * {@link MovableStorageAdapter}), that capability's contract is proven too — implementing a capability
26
+ * partially or unsafely fails conformance rather than silently escaping it.
27
+ */
28
+ export async function assertStorageAdapterConformance(options) {
29
+ let storage;
30
+ try {
31
+ storage = await options.createAdapter();
32
+ }
33
+ catch (error) {
34
+ return fail("construction", "createAdapter failed", error);
35
+ }
36
+ const a = "nifra-conformance/a/one.txt";
37
+ const b = "nifra-conformance/a/two.bin";
38
+ const c = "nifra-conformance/b/three.bin";
39
+ try {
40
+ await storage.put(a, "one", {
41
+ contentType: "text/x-nifra-conformance",
42
+ metadata: { owner: "nifra", purpose: "conformance" },
43
+ });
44
+ const object = (await storage.get(a)) ?? fail("round trip", "put object was not readable");
45
+ if (!(object.body instanceof Uint8Array))
46
+ fail("round trip", "body must be Uint8Array");
47
+ if (decode(object.body) !== "one" || object.size !== 3) {
48
+ fail("round trip", "body or size changed during round-trip");
49
+ }
50
+ if (object.contentType !== "text/x-nifra-conformance") {
51
+ fail("metadata", "contentType did not round-trip");
52
+ }
53
+ if (object.metadata?.owner !== "nifra" || object.metadata.purpose !== "conformance") {
54
+ fail("metadata", "custom metadata did not round-trip");
55
+ }
56
+ if (!(await storage.exists(a)))
57
+ fail("exists", "stored key was reported missing");
58
+ if (await storage.exists("nifra-conformance/missing")) {
59
+ fail("exists", "missing key was reported present");
60
+ }
61
+ if ((await storage.get("nifra-conformance/missing")) !== null) {
62
+ fail("missing", "get must return null for a missing key");
63
+ }
64
+ await storage.put(a, "overwritten");
65
+ const overwritten = await storage.get(a);
66
+ if (overwritten === null || decode(overwritten.body) !== "overwritten") {
67
+ fail("overwrite", "put did not replace the existing body");
68
+ }
69
+ await storage.put(b, "two");
70
+ await storage.put(c, "three");
71
+ const prefixed = [...(await storage.list({ prefix: "nifra-conformance/a/" }))].sort();
72
+ if (prefixed.length !== 2 || prefixed[0] !== a || prefixed[1] !== b) {
73
+ fail("list prefix", "prefix listing returned unexpected keys");
74
+ }
75
+ if ((await storage.list({ prefix: "nifra-conformance/", limit: 1 })).length !== 1) {
76
+ fail("list limit", "limit was not respected");
77
+ }
78
+ const unsafeCalls = [
79
+ ["put", () => storage.put("../escape", "x")],
80
+ ["get", () => storage.get("../escape")],
81
+ ["delete", () => storage.delete("../escape")],
82
+ ["exists", () => storage.exists("../escape")],
83
+ ];
84
+ if (hasPaging(storage)) {
85
+ const paged = storage;
86
+ // The cursor walk must reproduce exactly the keys list() reports, without duplicates,
87
+ // and must terminate by omitting the cursor on the final page.
88
+ const all = [...(await paged.list({ prefix: "nifra-conformance/" }))].sort();
89
+ const walked = [];
90
+ let cursor;
91
+ for (let page = 0; page <= all.length; page++) {
92
+ const result = await paged.listPage({
93
+ prefix: "nifra-conformance/",
94
+ limit: 1,
95
+ ...(cursor !== undefined ? { cursor } : {}),
96
+ });
97
+ if (result.keys.length > 1)
98
+ fail("listPage limit", "page exceeded the requested limit");
99
+ walked.push(...result.keys);
100
+ cursor = result.cursor;
101
+ if (cursor === undefined)
102
+ break;
103
+ }
104
+ if (cursor !== undefined)
105
+ fail("listPage cursor", "cursor never terminated");
106
+ const sortedWalk = [...walked].sort();
107
+ if (new Set(walked).size !== walked.length) {
108
+ fail("listPage cursor", "cursor walk returned duplicate keys");
109
+ }
110
+ if (sortedWalk.length !== all.length || sortedWalk.some((key, i) => key !== all[i])) {
111
+ fail("listPage cursor", "cursor walk and list() disagree on the key set");
112
+ }
113
+ }
114
+ if (hasMove(storage)) {
115
+ const movable = storage;
116
+ const copied = "nifra-conformance/b/copied.bin";
117
+ const moved = "nifra-conformance/b/moved.bin";
118
+ await movable.copy(c, copied);
119
+ const copyTarget = await movable.get(copied);
120
+ const copySource = await movable.get(c);
121
+ if (copyTarget === null || decode(copyTarget.body) !== "three") {
122
+ fail("copy", "copied object is missing or has different bytes");
123
+ }
124
+ if (copySource === null)
125
+ fail("copy", "copy must not remove the source object");
126
+ await movable.move(copied, moved);
127
+ const moveTarget = await movable.get(moved);
128
+ if (moveTarget === null || decode(moveTarget.body) !== "three") {
129
+ fail("move", "moved object is missing or has different bytes");
130
+ }
131
+ if ((await movable.exists(copied)) || (await movable.get(copied)) !== null) {
132
+ fail("move", "move must remove the source object");
133
+ }
134
+ await movable.delete(moved);
135
+ unsafeCalls.push(["copy source", () => movable.copy("../escape", copied)], ["copy destination", () => movable.copy(c, "../escape")], ["move source", () => movable.move("../escape", moved)], ["move destination", () => movable.move(c, "../escape")]);
136
+ }
137
+ if (hasPresign(storage)) {
138
+ const presignable = storage;
139
+ for (const operation of ["get", "put"]) {
140
+ const presigned = await presignable.presign(a, operation, {
141
+ expiresInSeconds: 60,
142
+ ...(operation === "put" ? { contentType: "text/plain", contentLength: 3 } : {}),
143
+ });
144
+ if (typeof presigned.url !== "string" || presigned.url.length === 0) {
145
+ fail("presign", `${operation} presign did not return a URL`);
146
+ }
147
+ if (presigned.expiresAt !== undefined && presigned.expiresAt.getTime() <= Date.now()) {
148
+ fail("presign", `${operation} presign returned an already-expired URL`);
149
+ }
150
+ }
151
+ unsafeCalls.push(["presign", () => presignable.presign("../escape", "get")]);
152
+ }
153
+ for (const [method, call] of unsafeCalls) {
154
+ try {
155
+ await call();
156
+ fail("key safety", `${method} accepted a traversal key`);
157
+ }
158
+ catch (error) {
159
+ if (error instanceof StorageAdapterConformanceError)
160
+ throw error;
161
+ }
162
+ }
163
+ await storage.delete(a);
164
+ if ((await storage.get(a)) !== null || (await storage.exists(a))) {
165
+ fail("delete", "deleted object remained readable");
166
+ }
167
+ await storage.delete(a);
168
+ }
169
+ catch (error) {
170
+ if (error instanceof StorageAdapterConformanceError)
171
+ throw error;
172
+ fail("operation", "adapter operation rejected unexpectedly", error);
173
+ }
174
+ finally {
175
+ await Promise.allSettled([storage.delete(a), storage.delete(b), storage.delete(c)]);
176
+ }
177
+ }
178
+ //# sourceMappingURL=conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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"}
package/dist/file.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { type ListOptions, type PutOptions, type StorageAdapter, type StorageData, type StorageObject } from "./types.js";
2
2
  export declare class FileStorage implements StorageAdapter {
3
3
  private readonly root;
4
+ private readonly metadataRoot;
4
5
  constructor(root: string);
6
+ private metadataPathFor;
5
7
  /** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
6
8
  private pathFor;
7
- put(key: string, data: StorageData, _options?: PutOptions): Promise<void>;
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;
14
+ put(key: string, data: StorageData, options?: PutOptions): Promise<void>;
8
15
  get(key: string): Promise<StorageObject | null>;
9
16
  delete(key: string): Promise<void>;
10
17
  exists(key: string): Promise<boolean>;
@@ -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;gBAEjB,IAAI,EAAE,MAAM;IAIxB,wHAAwH;IACxH,OAAO,CAAC,OAAO;IAST,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAc/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASrC,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
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Local-filesystem {@link StorageAdapter} for long-running servers (Bun/Node/Deno). Keys map to paths
3
3
  * under `root`; `assertSafeKey` plus a resolved-path containment check keep writes inside `root` (no
4
- * traversal). Bytes-only: `contentType` is inferred from the key's extension on read, and custom metadata
5
- * is NOT persisted — use `MemoryStorage`/`R2Storage` (or your own adapter) if you need metadata round-tripped.
4
+ * traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
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";
@@ -34,8 +35,20 @@ function inferContentType(key) {
34
35
  }
35
36
  export class FileStorage {
36
37
  root;
38
+ metadataRoot;
37
39
  constructor(root) {
38
40
  this.root = resolve(root);
41
+ // Keep bookkeeping outside the object tree so every otherwise-safe key remains usable, including
42
+ // `.nifra-metadata/*`, and list() never needs a reserved-prefix exception.
43
+ this.metadataRoot = `${this.root}.nifra-metadata`;
44
+ }
45
+ metadataPathFor(key) {
46
+ assertSafeKey(key);
47
+ const full = resolve(this.metadataRoot, `${key}.json`);
48
+ if (!full.startsWith(this.metadataRoot + sep)) {
49
+ throw new StorageKeyError(`storage metadata key ${JSON.stringify(key)} escapes the storage root`);
50
+ }
51
+ return full;
39
52
  }
40
53
  /** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
41
54
  pathFor(key) {
@@ -46,35 +59,128 @@ export class FileStorage {
46
59
  }
47
60
  return full;
48
61
  }
49
- async put(key, data, _options = {}) {
50
- const path = this.pathFor(key);
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);
51
98
  await mkdir(dirname(path), { recursive: true });
52
- await writeFile(path, toBytes(data));
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
+ }
115
+ async put(key, data, options = {}) {
116
+ const path = this.pathFor(key);
117
+ await this.writeContained(this.root, path, key, toBytes(data));
118
+ const metadataPath = this.metadataPathFor(key);
119
+ if (options.contentType !== undefined || options.metadata !== undefined) {
120
+ await this.writeContained(this.metadataRoot, metadataPath, key, JSON.stringify({
121
+ ...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
122
+ ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
123
+ }));
124
+ }
125
+ else {
126
+ await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
127
+ await rm(metadataPath, { force: true });
128
+ }
53
129
  }
54
130
  async get(key) {
55
131
  const path = this.pathFor(key);
132
+ await this.assertNoSymlinkPath(this.root, path, key);
56
133
  let body;
57
134
  try {
58
135
  body = new Uint8Array(await readFile(path));
59
136
  }
60
- catch {
61
- return null; // ENOENT (or unreadable) → treated as missing
137
+ catch (error) {
138
+ if (error.code === "ENOENT")
139
+ return null;
140
+ throw error;
62
141
  }
63
- const contentType = inferContentType(key);
64
- return contentType === undefined
65
- ? { body, size: body.byteLength }
66
- : { body, size: body.byteLength, contentType };
142
+ let stored = {};
143
+ try {
144
+ const metadataPath = this.metadataPathFor(key);
145
+ await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key);
146
+ stored = JSON.parse(await readFile(metadataPath, "utf8"));
147
+ }
148
+ catch (error) {
149
+ if (error instanceof StorageKeyError)
150
+ throw error;
151
+ if (error.code !== "ENOENT" && !(error instanceof SyntaxError)) {
152
+ throw error;
153
+ }
154
+ // Objects created before sidecar support still infer a useful MIME type from their extension.
155
+ }
156
+ const contentType = stored.contentType ?? inferContentType(key);
157
+ return {
158
+ body,
159
+ size: body.byteLength,
160
+ ...(contentType !== undefined ? { contentType } : {}),
161
+ ...(stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
162
+ };
67
163
  }
68
164
  async delete(key) {
69
- await rm(this.pathFor(key), { force: true });
165
+ const path = this.pathFor(key);
166
+ const metadataPath = this.metadataPathFor(key);
167
+ await Promise.all([
168
+ this.assertNoSymlinkPath(this.root, path, key),
169
+ this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
170
+ ]);
171
+ await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })]);
70
172
  }
71
173
  async exists(key) {
174
+ const path = this.pathFor(key);
175
+ await this.assertNoSymlinkPath(this.root, path, key);
72
176
  try {
73
- await stat(this.pathFor(key));
177
+ await stat(path);
74
178
  return true;
75
179
  }
76
- catch {
77
- return false;
180
+ catch (error) {
181
+ if (error.code === "ENOENT")
182
+ return false;
183
+ throw error;
78
184
  }
79
185
  }
80
186
  async list(options = {}) {
@@ -99,7 +205,9 @@ export class FileStorage {
99
205
  const full = join(dir, name);
100
206
  // POSIX-join the key segments so listed keys are portable regardless of the host separator.
101
207
  const key = prefix === "" ? name : posix.join(prefix, name);
102
- const info = await stat(full);
208
+ const info = await lstat(full);
209
+ if (info.isSymbolicLink())
210
+ continue;
103
211
  if (info.isDirectory())
104
212
  await this.walk(full, key, out);
105
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/dist/index.d.ts CHANGED
@@ -9,9 +9,10 @@
9
9
  * await storage.put("avatars/u1.png", bytes, { contentType: "image/png" })
10
10
  * const object = await storage.get("avatars/u1.png")
11
11
  */
12
+ export { assertStorageAdapterConformance, StorageAdapterConformanceError, type StorageAdapterConformanceOptions, } from "./conformance.js";
12
13
  export { FileStorage } from "./file.js";
13
14
  export { assertSafeKey, StorageKeyError } from "./key.js";
14
15
  export { MemoryStorage } from "./memory.js";
15
16
  export { type R2BucketLike, type R2ObjectLike, R2Storage } from "./r2.js";
16
- export { type ListOptions, type PutOptions, type StorageAdapter, type StorageData, type StorageObject, toBytes, } from "./types.js";
17
+ export { type ListOptions, type MovableStorageAdapter, type PagedStorageAdapter, type PresignableStorageAdapter, type PutOptions, type StorageAdapter, type StorageData, type StorageListPage, type StorageListPageOptions, type StorageObject, type StoragePresignedUrl, type StoragePresignOperation, type StoragePresignOptions, toBytes, } from "./types.js";
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,OAAO,GACR,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,EAC9B,KAAK,gCAAgC,GACtC,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,OAAO,GACR,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * await storage.put("avatars/u1.png", bytes, { contentType: "image/png" })
10
10
  * const object = await storage.get("avatars/u1.png")
11
11
  */
12
+ export { assertStorageAdapterConformance, StorageAdapterConformanceError, } from "./conformance.js";
12
13
  export { FileStorage } from "./file.js";
13
14
  export { assertSafeKey, StorageKeyError } from "./key.js";
14
15
  export { MemoryStorage } from "./memory.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAwC,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EAML,OAAO,GACR,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,GAE/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAwC,SAAS,EAAE,MAAM,SAAS,CAAA;AACzE,OAAO,EAcL,OAAO,GACR,MAAM,YAAY,CAAA"}
package/dist/key.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * portable across them, and so the check can't be forgotten on the one adapter where it's a vulnerability.
6
6
  */
7
7
  export declare class StorageKeyError extends Error {
8
- readonly name = "StorageKeyError";
8
+ constructor(message: string);
9
9
  }
10
10
  /** Throw {@link StorageKeyError} unless `key` is a safe relative storage key. */
11
11
  export declare function assertSafeKey(key: string): void;
package/dist/key.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,SAAkB,IAAI,qBAAoB;CAC3C;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAc/C"}
1
+ {"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAc/C"}
package/dist/key.js CHANGED
@@ -5,7 +5,10 @@
5
5
  * portable across them, and so the check can't be forgotten on the one adapter where it's a vulnerability.
6
6
  */
7
7
  export class StorageKeyError extends Error {
8
- name = "StorageKeyError";
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "StorageKeyError";
11
+ }
9
12
  }
10
13
  /** Throw {@link StorageKeyError} unless `key` is a safe relative storage key. */
11
14
  export function assertSafeKey(key) {
package/dist/key.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACtB,IAAI,GAAG,iBAAiB,CAAA;CAC3C;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,eAAe,CAAC,yCAAyC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAA;IACnF,CAAC;IACD,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,eAAe,CACvB,sBAAsB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,gCAAgC,CAC1E,CAAA;IACH,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,IAAI,eAAe,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;QAC9F,CAAC;IACH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;IAC/B,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,eAAe,CAAC,yCAAyC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAA;IACnF,CAAC;IACD,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,eAAe,CACvB,sBAAsB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,gCAAgC,CAC1E,CAAA;IACH,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,MAAM,IAAI,eAAe,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;QAC9F,CAAC;IACH,CAAC;AACH,CAAC"}
package/dist/memory.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type ListOptions, type PutOptions, type StorageAdapter, type StorageData, type StorageObject } from "./types.js";
2
2
  export declare class MemoryStorage implements StorageAdapter {
3
3
  private readonly objects;
4
+ constructor();
4
5
  put(key: string, data: StorageData, options?: PutOptions): Promise<void>;
5
6
  get(key: string): Promise<StorageObject | null>;
6
7
  delete(key: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAA;AAQnB,qBAAa,aAAc,YAAW,cAAc;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAE7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAY/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAMzD"}
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAA;AAQnB,qBAAa,aAAc,YAAW,cAAc;IAClD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;;IAMtC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAY/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKrC,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAMzD"}
package/dist/memory.js CHANGED
@@ -6,7 +6,10 @@
6
6
  import { assertSafeKey } from "./key.js";
7
7
  import { toBytes, } from "./types.js";
8
8
  export class MemoryStorage {
9
- objects = new Map();
9
+ objects;
10
+ constructor() {
11
+ this.objects = new Map();
12
+ }
10
13
  async put(key, data, options = {}) {
11
14
  assertSafeKey(key);
12
15
  const entry = { body: toBytes(data) };
@@ -1 +1 @@
1
- {"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAML,OAAO,GACR,MAAM,YAAY,CAAA;AAQnB,MAAM,OAAO,aAAa;IACP,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAA;IAEnD,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,IAAiB,EAAE,UAAsB,EAAE;QAChE,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,KAAK,GAAU,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAA;QAC5C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;YACpB,GAAG,KAAK;YACR,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClF,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1E,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAA;QACpC,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU;YAC3B,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtE,CAAA;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAuB,EAAE;QAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;QACnC,IAAI,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC9E,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACpE,OAAO,IAAI,CAAA;IACb,CAAC;CACF"}
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAML,OAAO,GACR,MAAM,YAAY,CAAA;AAQnB,MAAM,OAAO,aAAa;IACP,OAAO,CAAoB;IAE5C;QACE,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,IAAiB,EAAE,UAAsB,EAAE;QAChE,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,KAAK,GAAU,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAA;QAC5C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;YACpB,GAAG,KAAK;YACR,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClF,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1E,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAA;QACpC,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU;YAC3B,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtE,CAAA;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,aAAa,CAAC,GAAG,CAAC,CAAA;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAAuB,EAAE;QAClC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;QACnC,IAAI,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC9E,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;QACpE,OAAO,IAAI,CAAA;IACb,CAAC;CACF"}
package/dist/types.d.ts CHANGED
@@ -25,6 +25,31 @@ export interface ListOptions {
25
25
  /** Cap the number of keys returned. */
26
26
  readonly limit?: number;
27
27
  }
28
+ /** One page of keys from stores that expose cursor-based listing. */
29
+ export interface StorageListPage {
30
+ readonly keys: readonly string[];
31
+ /** Opaque cursor for the next page; absent when this is the final page. */
32
+ readonly cursor?: string;
33
+ }
34
+ /** Cursor-aware listing options. `cursor` is adapter-owned and must be treated as opaque. */
35
+ export interface StorageListPageOptions extends ListOptions {
36
+ readonly cursor?: string;
37
+ }
38
+ /** Operation represented by a presigned storage URL. */
39
+ export type StoragePresignOperation = "get" | "put";
40
+ /** Mechanical constraints applied while minting a presigned URL. */
41
+ export interface StoragePresignOptions {
42
+ readonly expiresInSeconds?: number;
43
+ /** For PUT URLs, pin the declared MIME type. */
44
+ readonly contentType?: string;
45
+ /** For PUT URLs, pin the exact request-body size. */
46
+ readonly contentLength?: number;
47
+ }
48
+ /** A provider-minted URL and its known expiry. */
49
+ export interface StoragePresignedUrl {
50
+ readonly url: string;
51
+ readonly expiresAt?: Date;
52
+ }
28
53
  /**
29
54
  * A blob store keyed by string. Keys are POSIX-ish paths (`avatars/u1.png`); every adapter rejects unsafe
30
55
  * keys (absolute, `..` traversal, NUL, backslash) so a key valid in one adapter is valid in all. All
@@ -42,6 +67,19 @@ export interface StorageAdapter {
42
67
  /** List stored keys (optionally filtered by prefix + capped). */
43
68
  list(options?: ListOptions): Promise<string[]>;
44
69
  }
70
+ /** Optional cursor-listing capability. Kept out of {@link StorageAdapter} for simple stores. */
71
+ export interface PagedStorageAdapter extends StorageAdapter {
72
+ listPage(options?: StorageListPageOptions): Promise<StorageListPage>;
73
+ }
74
+ /** Optional provider-side URL-signing capability. Asset sensitivity and TTL policy stay with callers. */
75
+ export interface PresignableStorageAdapter extends StorageAdapter {
76
+ presign(key: string, operation: StoragePresignOperation, options?: StoragePresignOptions): Promise<StoragePresignedUrl>;
77
+ }
78
+ /** Optional server-side copy/move capability. */
79
+ export interface MovableStorageAdapter extends StorageAdapter {
80
+ copy(sourceKey: string, destinationKey: string): Promise<void>;
81
+ move(sourceKey: string, destinationKey: string): Promise<void>;
82
+ }
45
83
  /** Normalize any accepted payload to bytes. */
46
84
  export declare function toBytes(data: StorageData): Uint8Array;
47
85
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qEAAqE;AACrE,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,CAAA;AAE3D,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACrD;AAED,sGAAsG;AACtG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACrD;AAED,MAAM,WAAW,WAAW;IAC1B,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,uCAAuC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,+DAA+D;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC/C,+CAA+C;IAC/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,yCAAyC;IACzC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,iEAAiE;IACjE,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CAC/C;AAED,+CAA+C;AAC/C,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAIrD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6CH,+CAA+C;AAC/C,MAAM,UAAU,OAAO,CAAC,IAAiB;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnE,IAAI,IAAI,YAAY,UAAU;QAAE,OAAO,IAAI,CAAA;IAC3C,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;AAC7B,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA+FH,+CAA+C;AAC/C,MAAM,UAAU,OAAO,CAAC,IAAiB;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnE,IAAI,IAAI,YAAY,UAAU;QAAE,OAAO,IAAI,CAAA;IAC3C,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;AAC7B,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/storage",
3
- "version": "1.3.1",
4
- "description": "Blob storage for nifra — one StorageAdapter interface with memory, local-filesystem, and Cloudflare R2 adapters (key-traversal-safe). The persistence half of @nifrajs/uploads. Dependency-free; bring your own adapter for S3/GCS.",
3
+ "version": "1.5.0",
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",
7
7
  "typescript",
@@ -0,0 +1,210 @@
1
+ import type {
2
+ MovableStorageAdapter,
3
+ PagedStorageAdapter,
4
+ PresignableStorageAdapter,
5
+ StorageAdapter,
6
+ } from "./types.ts"
7
+
8
+ /** Construction and cleanup hooks for {@link assertStorageAdapterConformance}. */
9
+ export interface StorageAdapterConformanceOptions {
10
+ /** Return an empty, isolated adapter for this run. */
11
+ readonly createAdapter: () => StorageAdapter | Promise<StorageAdapter>
12
+ }
13
+
14
+ /** A failed invariant reported by {@link assertStorageAdapterConformance}. */
15
+ export class StorageAdapterConformanceError extends Error {
16
+ constructor(
17
+ readonly check: string,
18
+ message: string,
19
+ cause?: unknown,
20
+ ) {
21
+ super(`StorageAdapter conformance failed (${check}): ${message}`, { cause })
22
+ this.name = "StorageAdapterConformanceError"
23
+ }
24
+ }
25
+
26
+ const fail = (check: string, message: string, cause?: unknown): never => {
27
+ throw new StorageAdapterConformanceError(check, message, cause)
28
+ }
29
+
30
+ const decode = (bytes: Uint8Array): string => new TextDecoder().decode(bytes)
31
+
32
+ const hasPaging = (storage: StorageAdapter): storage is PagedStorageAdapter =>
33
+ typeof (storage as PagedStorageAdapter).listPage === "function"
34
+
35
+ const hasPresign = (storage: StorageAdapter): storage is PresignableStorageAdapter =>
36
+ typeof (storage as PresignableStorageAdapter).presign === "function"
37
+
38
+ const hasMove = (storage: StorageAdapter): storage is MovableStorageAdapter =>
39
+ typeof (storage as MovableStorageAdapter).copy === "function" &&
40
+ typeof (storage as MovableStorageAdapter).move === "function"
41
+
42
+ /**
43
+ * Execute the observable {@link StorageAdapter} contract without depending on a test runner.
44
+ *
45
+ * The adapter must be isolated because this writes deterministic conformance keys. The promise resolves
46
+ * only after proving byte + metadata round-trips, overwrite, missing/delete semantics, prefix/limit listing,
47
+ * and uniform unsafe-key rejection across every key-taking method. When the adapter also implements an
48
+ * optional capability ({@link PagedStorageAdapter}, {@link PresignableStorageAdapter},
49
+ * {@link MovableStorageAdapter}), that capability's contract is proven too — implementing a capability
50
+ * partially or unsafely fails conformance rather than silently escaping it.
51
+ */
52
+ export async function assertStorageAdapterConformance(
53
+ options: StorageAdapterConformanceOptions,
54
+ ): Promise<void> {
55
+ let storage: StorageAdapter
56
+ try {
57
+ storage = await options.createAdapter()
58
+ } catch (error) {
59
+ return fail("construction", "createAdapter failed", error)
60
+ }
61
+
62
+ const a = "nifra-conformance/a/one.txt"
63
+ const b = "nifra-conformance/a/two.bin"
64
+ const c = "nifra-conformance/b/three.bin"
65
+
66
+ try {
67
+ await storage.put(a, "one", {
68
+ contentType: "text/x-nifra-conformance",
69
+ metadata: { owner: "nifra", purpose: "conformance" },
70
+ })
71
+ const object = (await storage.get(a)) ?? fail("round trip", "put object was not readable")
72
+ if (!(object.body instanceof Uint8Array)) fail("round trip", "body must be Uint8Array")
73
+ if (decode(object.body) !== "one" || object.size !== 3) {
74
+ fail("round trip", "body or size changed during round-trip")
75
+ }
76
+ if (object.contentType !== "text/x-nifra-conformance") {
77
+ fail("metadata", "contentType did not round-trip")
78
+ }
79
+ if (object.metadata?.owner !== "nifra" || object.metadata.purpose !== "conformance") {
80
+ fail("metadata", "custom metadata did not round-trip")
81
+ }
82
+
83
+ if (!(await storage.exists(a))) fail("exists", "stored key was reported missing")
84
+ if (await storage.exists("nifra-conformance/missing")) {
85
+ fail("exists", "missing key was reported present")
86
+ }
87
+ if ((await storage.get("nifra-conformance/missing")) !== null) {
88
+ fail("missing", "get must return null for a missing key")
89
+ }
90
+
91
+ await storage.put(a, "overwritten")
92
+ const overwritten = await storage.get(a)
93
+ if (overwritten === null || decode(overwritten.body) !== "overwritten") {
94
+ fail("overwrite", "put did not replace the existing body")
95
+ }
96
+
97
+ await storage.put(b, "two")
98
+ await storage.put(c, "three")
99
+ const prefixed = [...(await storage.list({ prefix: "nifra-conformance/a/" }))].sort()
100
+ if (prefixed.length !== 2 || prefixed[0] !== a || prefixed[1] !== b) {
101
+ fail("list prefix", "prefix listing returned unexpected keys")
102
+ }
103
+ if ((await storage.list({ prefix: "nifra-conformance/", limit: 1 })).length !== 1) {
104
+ fail("list limit", "limit was not respected")
105
+ }
106
+
107
+ const unsafeCalls: Array<readonly [string, () => Promise<unknown>]> = [
108
+ ["put", () => storage.put("../escape", "x")],
109
+ ["get", () => storage.get("../escape")],
110
+ ["delete", () => storage.delete("../escape")],
111
+ ["exists", () => storage.exists("../escape")],
112
+ ]
113
+ if (hasPaging(storage)) {
114
+ const paged = storage
115
+
116
+ // The cursor walk must reproduce exactly the keys list() reports, without duplicates,
117
+ // and must terminate by omitting the cursor on the final page.
118
+ const all = [...(await paged.list({ prefix: "nifra-conformance/" }))].sort()
119
+ const walked: string[] = []
120
+ let cursor: string | undefined
121
+ for (let page = 0; page <= all.length; page++) {
122
+ const result = await paged.listPage({
123
+ prefix: "nifra-conformance/",
124
+ limit: 1,
125
+ ...(cursor !== undefined ? { cursor } : {}),
126
+ })
127
+ if (result.keys.length > 1) fail("listPage limit", "page exceeded the requested limit")
128
+ walked.push(...result.keys)
129
+ cursor = result.cursor
130
+ if (cursor === undefined) break
131
+ }
132
+ if (cursor !== undefined) fail("listPage cursor", "cursor never terminated")
133
+ const sortedWalk = [...walked].sort()
134
+ if (new Set(walked).size !== walked.length) {
135
+ fail("listPage cursor", "cursor walk returned duplicate keys")
136
+ }
137
+ if (sortedWalk.length !== all.length || sortedWalk.some((key, i) => key !== all[i])) {
138
+ fail("listPage cursor", "cursor walk and list() disagree on the key set")
139
+ }
140
+ }
141
+
142
+ if (hasMove(storage)) {
143
+ const movable = storage
144
+ const copied = "nifra-conformance/b/copied.bin"
145
+ const moved = "nifra-conformance/b/moved.bin"
146
+
147
+ await movable.copy(c, copied)
148
+ const copyTarget = await movable.get(copied)
149
+ const copySource = await movable.get(c)
150
+ if (copyTarget === null || decode(copyTarget.body) !== "three") {
151
+ fail("copy", "copied object is missing or has different bytes")
152
+ }
153
+ if (copySource === null) fail("copy", "copy must not remove the source object")
154
+
155
+ await movable.move(copied, moved)
156
+ const moveTarget = await movable.get(moved)
157
+ if (moveTarget === null || decode(moveTarget.body) !== "three") {
158
+ fail("move", "moved object is missing or has different bytes")
159
+ }
160
+ if ((await movable.exists(copied)) || (await movable.get(copied)) !== null) {
161
+ fail("move", "move must remove the source object")
162
+ }
163
+ await movable.delete(moved)
164
+
165
+ unsafeCalls.push(
166
+ ["copy source", () => movable.copy("../escape", copied)],
167
+ ["copy destination", () => movable.copy(c, "../escape")],
168
+ ["move source", () => movable.move("../escape", moved)],
169
+ ["move destination", () => movable.move(c, "../escape")],
170
+ )
171
+ }
172
+
173
+ if (hasPresign(storage)) {
174
+ const presignable = storage
175
+ for (const operation of ["get", "put"] as const) {
176
+ const presigned = await presignable.presign(a, operation, {
177
+ expiresInSeconds: 60,
178
+ ...(operation === "put" ? { contentType: "text/plain", contentLength: 3 } : {}),
179
+ })
180
+ if (typeof presigned.url !== "string" || presigned.url.length === 0) {
181
+ fail("presign", `${operation} presign did not return a URL`)
182
+ }
183
+ if (presigned.expiresAt !== undefined && presigned.expiresAt.getTime() <= Date.now()) {
184
+ fail("presign", `${operation} presign returned an already-expired URL`)
185
+ }
186
+ }
187
+ unsafeCalls.push(["presign", () => presignable.presign("../escape", "get")])
188
+ }
189
+
190
+ for (const [method, call] of unsafeCalls) {
191
+ try {
192
+ await call()
193
+ fail("key safety", `${method} accepted a traversal key`)
194
+ } catch (error) {
195
+ if (error instanceof StorageAdapterConformanceError) throw error
196
+ }
197
+ }
198
+
199
+ await storage.delete(a)
200
+ if ((await storage.get(a)) !== null || (await storage.exists(a))) {
201
+ fail("delete", "deleted object remained readable")
202
+ }
203
+ await storage.delete(a)
204
+ } catch (error) {
205
+ if (error instanceof StorageAdapterConformanceError) throw error
206
+ fail("operation", "adapter operation rejected unexpectedly", error)
207
+ } finally {
208
+ await Promise.allSettled([storage.delete(a), storage.delete(b), storage.delete(c)])
209
+ }
210
+ }
package/src/file.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Local-filesystem {@link StorageAdapter} for long-running servers (Bun/Node/Deno). Keys map to paths
3
3
  * under `root`; `assertSafeKey` plus a resolved-path containment check keep writes inside `root` (no
4
- * traversal). Bytes-only: `contentType` is inferred from the key's extension on read, and custom metadata
5
- * is NOT persisted — use `MemoryStorage`/`R2Storage` (or your own adapter) if you need metadata round-tripped.
4
+ * traversal). Explicit content type and custom metadata are persisted in an adjacent sidecar tree;
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 {
@@ -44,9 +45,24 @@ function inferContentType(key: string): string | undefined {
44
45
 
45
46
  export class FileStorage implements StorageAdapter {
46
47
  private readonly root: string
48
+ private readonly metadataRoot: string
47
49
 
48
50
  constructor(root: string) {
49
51
  this.root = resolve(root)
52
+ // Keep bookkeeping outside the object tree so every otherwise-safe key remains usable, including
53
+ // `.nifra-metadata/*`, and list() never needs a reserved-prefix exception.
54
+ this.metadataRoot = `${this.root}.nifra-metadata`
55
+ }
56
+
57
+ private metadataPathFor(key: string): string {
58
+ assertSafeKey(key)
59
+ const full = resolve(this.metadataRoot, `${key}.json`)
60
+ if (!full.startsWith(this.metadataRoot + sep)) {
61
+ throw new StorageKeyError(
62
+ `storage metadata key ${JSON.stringify(key)} escapes the storage root`,
63
+ )
64
+ }
65
+ return full
50
66
  }
51
67
 
52
68
  /** Resolve `key` to an absolute path, asserting it stays inside `root` (defense-in-depth on top of `assertSafeKey`). */
@@ -59,36 +75,132 @@ export class FileStorage implements StorageAdapter {
59
75
  return full
60
76
  }
61
77
 
62
- async put(key: string, data: StorageData, _options: PutOptions = {}): Promise<void> {
63
- const path = this.pathFor(key)
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)
64
115
  await mkdir(dirname(path), { recursive: true })
65
- await writeFile(path, toBytes(data))
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
+
134
+ async put(key: string, data: StorageData, options: PutOptions = {}): Promise<void> {
135
+ const path = this.pathFor(key)
136
+ await this.writeContained(this.root, path, key, toBytes(data))
137
+ const metadataPath = this.metadataPathFor(key)
138
+ if (options.contentType !== undefined || options.metadata !== undefined) {
139
+ await this.writeContained(
140
+ this.metadataRoot,
141
+ metadataPath,
142
+ key,
143
+ JSON.stringify({
144
+ ...(options.contentType !== undefined ? { contentType: options.contentType } : {}),
145
+ ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
146
+ }),
147
+ )
148
+ } else {
149
+ await this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key)
150
+ await rm(metadataPath, { force: true })
151
+ }
66
152
  }
67
153
 
68
154
  async get(key: string): Promise<StorageObject | null> {
69
155
  const path = this.pathFor(key)
156
+ await this.assertNoSymlinkPath(this.root, path, key)
70
157
  let body: Uint8Array
71
158
  try {
72
159
  body = new Uint8Array(await readFile(path))
73
- } catch {
74
- return null // ENOENT (or unreadable) → treated as missing
160
+ } catch (error) {
161
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null
162
+ throw error
163
+ }
164
+ let stored: { contentType?: string; metadata?: Readonly<Record<string, string>> } = {}
165
+ try {
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
+ }
174
+ // Objects created before sidecar support still infer a useful MIME type from their extension.
175
+ }
176
+ const contentType = stored.contentType ?? inferContentType(key)
177
+ return {
178
+ body,
179
+ size: body.byteLength,
180
+ ...(contentType !== undefined ? { contentType } : {}),
181
+ ...(stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
75
182
  }
76
- const contentType = inferContentType(key)
77
- return contentType === undefined
78
- ? { body, size: body.byteLength }
79
- : { body, size: body.byteLength, contentType }
80
183
  }
81
184
 
82
185
  async delete(key: string): Promise<void> {
83
- await rm(this.pathFor(key), { force: true })
186
+ const path = this.pathFor(key)
187
+ const metadataPath = this.metadataPathFor(key)
188
+ await Promise.all([
189
+ this.assertNoSymlinkPath(this.root, path, key),
190
+ this.assertNoSymlinkPath(this.metadataRoot, metadataPath, key),
191
+ ])
192
+ await Promise.all([rm(path, { force: true }), rm(metadataPath, { force: true })])
84
193
  }
85
194
 
86
195
  async exists(key: string): Promise<boolean> {
196
+ const path = this.pathFor(key)
197
+ await this.assertNoSymlinkPath(this.root, path, key)
87
198
  try {
88
- await stat(this.pathFor(key))
199
+ await stat(path)
89
200
  return true
90
- } catch {
91
- return false
201
+ } catch (error) {
202
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false
203
+ throw error
92
204
  }
93
205
  }
94
206
 
@@ -113,7 +225,8 @@ export class FileStorage implements StorageAdapter {
113
225
  const full = join(dir, name)
114
226
  // POSIX-join the key segments so listed keys are portable regardless of the host separator.
115
227
  const key = prefix === "" ? name : posix.join(prefix, name)
116
- const info = await stat(full)
228
+ const info = await lstat(full)
229
+ if (info.isSymbolicLink()) continue
117
230
  if (info.isDirectory()) await this.walk(full, key, out)
118
231
  else if (info.isFile()) out.push(key)
119
232
  }
package/src/index.ts CHANGED
@@ -10,15 +10,28 @@
10
10
  * const object = await storage.get("avatars/u1.png")
11
11
  */
12
12
 
13
+ export {
14
+ assertStorageAdapterConformance,
15
+ StorageAdapterConformanceError,
16
+ type StorageAdapterConformanceOptions,
17
+ } from "./conformance.ts"
13
18
  export { FileStorage } from "./file.ts"
14
19
  export { assertSafeKey, StorageKeyError } from "./key.ts"
15
20
  export { MemoryStorage } from "./memory.ts"
16
21
  export { type R2BucketLike, type R2ObjectLike, R2Storage } from "./r2.ts"
17
22
  export {
18
23
  type ListOptions,
24
+ type MovableStorageAdapter,
25
+ type PagedStorageAdapter,
26
+ type PresignableStorageAdapter,
19
27
  type PutOptions,
20
28
  type StorageAdapter,
21
29
  type StorageData,
30
+ type StorageListPage,
31
+ type StorageListPageOptions,
22
32
  type StorageObject,
33
+ type StoragePresignedUrl,
34
+ type StoragePresignOperation,
35
+ type StoragePresignOptions,
23
36
  toBytes,
24
37
  } from "./types.ts"
package/src/key.ts CHANGED
@@ -6,7 +6,10 @@
6
6
  */
7
7
 
8
8
  export class StorageKeyError extends Error {
9
- override readonly name = "StorageKeyError"
9
+ constructor(message: string) {
10
+ super(message)
11
+ this.name = "StorageKeyError"
12
+ }
10
13
  }
11
14
 
12
15
  /** Throw {@link StorageKeyError} unless `key` is a safe relative storage key. */
package/src/memory.ts CHANGED
@@ -20,7 +20,11 @@ interface Entry {
20
20
  }
21
21
 
22
22
  export class MemoryStorage implements StorageAdapter {
23
- private readonly objects = new Map<string, Entry>()
23
+ private readonly objects: Map<string, Entry>
24
+
25
+ constructor() {
26
+ this.objects = new Map()
27
+ }
24
28
 
25
29
  async put(key: string, data: StorageData, options: PutOptions = {}): Promise<void> {
26
30
  assertSafeKey(key)
package/src/types.ts CHANGED
@@ -30,6 +30,36 @@ export interface ListOptions {
30
30
  readonly limit?: number
31
31
  }
32
32
 
33
+ /** One page of keys from stores that expose cursor-based listing. */
34
+ export interface StorageListPage {
35
+ readonly keys: readonly string[]
36
+ /** Opaque cursor for the next page; absent when this is the final page. */
37
+ readonly cursor?: string
38
+ }
39
+
40
+ /** Cursor-aware listing options. `cursor` is adapter-owned and must be treated as opaque. */
41
+ export interface StorageListPageOptions extends ListOptions {
42
+ readonly cursor?: string
43
+ }
44
+
45
+ /** Operation represented by a presigned storage URL. */
46
+ export type StoragePresignOperation = "get" | "put"
47
+
48
+ /** Mechanical constraints applied while minting a presigned URL. */
49
+ export interface StoragePresignOptions {
50
+ readonly expiresInSeconds?: number
51
+ /** For PUT URLs, pin the declared MIME type. */
52
+ readonly contentType?: string
53
+ /** For PUT URLs, pin the exact request-body size. */
54
+ readonly contentLength?: number
55
+ }
56
+
57
+ /** A provider-minted URL and its known expiry. */
58
+ export interface StoragePresignedUrl {
59
+ readonly url: string
60
+ readonly expiresAt?: Date
61
+ }
62
+
33
63
  /**
34
64
  * A blob store keyed by string. Keys are POSIX-ish paths (`avatars/u1.png`); every adapter rejects unsafe
35
65
  * keys (absolute, `..` traversal, NUL, backslash) so a key valid in one adapter is valid in all. All
@@ -48,6 +78,26 @@ export interface StorageAdapter {
48
78
  list(options?: ListOptions): Promise<string[]>
49
79
  }
50
80
 
81
+ /** Optional cursor-listing capability. Kept out of {@link StorageAdapter} for simple stores. */
82
+ export interface PagedStorageAdapter extends StorageAdapter {
83
+ listPage(options?: StorageListPageOptions): Promise<StorageListPage>
84
+ }
85
+
86
+ /** Optional provider-side URL-signing capability. Asset sensitivity and TTL policy stay with callers. */
87
+ export interface PresignableStorageAdapter extends StorageAdapter {
88
+ presign(
89
+ key: string,
90
+ operation: StoragePresignOperation,
91
+ options?: StoragePresignOptions,
92
+ ): Promise<StoragePresignedUrl>
93
+ }
94
+
95
+ /** Optional server-side copy/move capability. */
96
+ export interface MovableStorageAdapter extends StorageAdapter {
97
+ copy(sourceKey: string, destinationKey: string): Promise<void>
98
+ move(sourceKey: string, destinationKey: string): Promise<void>
99
+ }
100
+
51
101
  /** Normalize any accepted payload to bytes. */
52
102
  export function toBytes(data: StorageData): Uint8Array {
53
103
  if (typeof data === "string") return new TextEncoder().encode(data)