@stacksjs/storage 0.70.45 → 0.70.54
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/LICENSE.md +21 -0
- package/dist/image.js +58 -0
- package/dist/index.js +2386 -1278
- package/dist/src/adapters/bun.d.ts +6 -4
- package/dist/src/adapters/index.d.ts +2 -0
- package/dist/src/adapters/local.d.ts +6 -4
- package/dist/src/adapters/memory.d.ts +4 -2
- package/dist/src/adapters/s3.d.ts +24 -7
- package/dist/src/adapters/scoped.d.ts +68 -0
- package/dist/src/facade.d.ts +14 -4
- package/dist/src/image.d.ts +55 -0
- package/dist/src/index.d.ts +34 -1
- package/dist/src/mime-verify.d.ts +65 -0
- package/dist/src/path-sanitize.d.ts +92 -0
- package/dist/src/put-file.d.ts +53 -0
- package/dist/src/s3-presigned-post.d.ts +52 -0
- package/dist/src/signed-url.d.ts +33 -1
- package/dist/src/types/filesystem.d.ts +31 -0
- package/dist/src/types.d.ts +90 -1
- package/package.json +7 -7
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import type { ChecksumOptions, DirectoryListing, FileContents, ListOptions, MimeTypeOptions, PublicUrlOptions, SignedUrlOptions, StatEntry, StorageAdapter, StorageAdapterConfig, TemporaryUrlOptions, Visibility } from '../types';
|
|
2
|
+
import type { ChecksumOptions, DirectoryListing, FileContents, GetStreamOptions, ListOptions, MimeTypeOptions, PublicUrlOptions, PutResult, PutStreamOptions, SignedUrlOptions, StatEntry, StorageAdapter, StorageAdapterConfig, TemporaryUrlOptions, Visibility } from '../types';
|
|
3
3
|
export declare function createBunStorage(config?: StorageAdapterConfig): BunStorageAdapter;
|
|
4
4
|
export declare class BunStorageAdapter implements StorageAdapter {
|
|
5
5
|
constructor(config?: StorageAdapterConfig);
|
|
6
|
-
write(path: string, contents: FileContents): Promise<
|
|
6
|
+
write(path: string, contents: FileContents): Promise<PutResult>;
|
|
7
7
|
read(path: string): Promise<FileContents>;
|
|
8
|
+
getStream(path: string, _options?: GetStreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
9
|
+
putStream(path: string, stream: ReadableStream<Uint8Array>, options?: PutStreamOptions): Promise<PutResult>;
|
|
8
10
|
readToString(path: string): Promise<string>;
|
|
9
11
|
readToBuffer(path: string): Promise<Buffer>;
|
|
10
12
|
readToUint8Array(path: string): Promise<Uint8Array>;
|
|
@@ -15,8 +17,8 @@ export declare class BunStorageAdapter implements StorageAdapter {
|
|
|
15
17
|
copyFile(from: string, to: string): Promise<void>;
|
|
16
18
|
stat(path: string): Promise<StatEntry>;
|
|
17
19
|
list(path: string, options?: ListOptions): DirectoryListing;
|
|
18
|
-
changeVisibility(
|
|
19
|
-
visibility(
|
|
20
|
+
changeVisibility(path: string, vis: Visibility): Promise<void>;
|
|
21
|
+
visibility(path: string): Promise<Visibility>;
|
|
20
22
|
fileExists(path: string): Promise<boolean>;
|
|
21
23
|
directoryExists(path: string): Promise<boolean>;
|
|
22
24
|
publicUrl(path: string, options?: PublicUrlOptions): Promise<string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { copyFile, stat } from 'node:fs/promises';
|
|
3
|
-
import type { ChecksumOptions, DirectoryListing, FileContents, ListOptions, MimeTypeOptions, PublicUrlOptions, SignedUrlOptions, StatEntry, StorageAdapter, StorageAdapterConfig, TemporaryUrlOptions, Visibility } from '../types';
|
|
3
|
+
import type { ChecksumOptions, DirectoryListing, FileContents, GetStreamOptions, ListOptions, MimeTypeOptions, PublicUrlOptions, PutResult, PutStreamOptions, SignedUrlOptions, StatEntry, StorageAdapter, StorageAdapterConfig, TemporaryUrlOptions, Visibility } from '../types';
|
|
4
4
|
/**
|
|
5
5
|
* Create a local storage adapter instance
|
|
6
6
|
*/
|
|
@@ -10,8 +10,10 @@ export declare function createLocalStorage(config?: StorageAdapterConfig): Local
|
|
|
10
10
|
*/
|
|
11
11
|
export declare class LocalStorageAdapter implements StorageAdapter {
|
|
12
12
|
constructor(config?: StorageAdapterConfig);
|
|
13
|
-
write(path: string, contents: FileContents): Promise<
|
|
13
|
+
write(path: string, contents: FileContents): Promise<PutResult>;
|
|
14
14
|
read(path: string): Promise<FileContents>;
|
|
15
|
+
getStream(path: string, options?: GetStreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
16
|
+
putStream(path: string, stream: ReadableStream<Uint8Array>, options?: PutStreamOptions): Promise<PutResult>;
|
|
15
17
|
readToString(path: string): Promise<string>;
|
|
16
18
|
readToBuffer(path: string): Promise<Buffer>;
|
|
17
19
|
readToUint8Array(path: string): Promise<Uint8Array>;
|
|
@@ -22,8 +24,8 @@ export declare class LocalStorageAdapter implements StorageAdapter {
|
|
|
22
24
|
copyFile(from: string, to: string): Promise<void>;
|
|
23
25
|
stat(path: string): Promise<StatEntry>;
|
|
24
26
|
list(path: string, options?: ListOptions): DirectoryListing;
|
|
25
|
-
changeVisibility(
|
|
26
|
-
visibility(
|
|
27
|
+
changeVisibility(path: string, vis: Visibility): Promise<void>;
|
|
28
|
+
visibility(path: string): Promise<Visibility>;
|
|
27
29
|
fileExists(path: string): Promise<boolean>;
|
|
28
30
|
directoryExists(path: string): Promise<boolean>;
|
|
29
31
|
publicUrl(path: string, options?: PublicUrlOptions): Promise<string>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import type { ChecksumOptions, DirectoryListing, FileContents, ListOptions, MimeTypeOptions, PublicUrlOptions, SignedUrlOptions, StatEntry, StorageAdapter, TemporaryUrlOptions, Visibility } from '../types';
|
|
2
|
+
import type { ChecksumOptions, DirectoryListing, FileContents, GetStreamOptions, ListOptions, MimeTypeOptions, PublicUrlOptions, PutResult, PutStreamOptions, SignedUrlOptions, StatEntry, StorageAdapter, TemporaryUrlOptions, Visibility } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Create an in-memory storage adapter instance
|
|
5
5
|
*/
|
|
@@ -9,8 +9,10 @@ export declare function createMemoryStorage(): InMemoryStorageAdapter;
|
|
|
9
9
|
*/
|
|
10
10
|
export declare class InMemoryStorageAdapter implements StorageAdapter {
|
|
11
11
|
constructor();
|
|
12
|
-
write(path: string, contents: FileContents): Promise<
|
|
12
|
+
write(path: string, contents: FileContents): Promise<PutResult>;
|
|
13
13
|
read(path: string): Promise<FileContents>;
|
|
14
|
+
getStream(path: string, _options?: GetStreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
15
|
+
putStream(path: string, stream: ReadableStream<Uint8Array>, options?: PutStreamOptions): Promise<PutResult>;
|
|
14
16
|
readToString(path: string): Promise<string>;
|
|
15
17
|
readToBuffer(path: string): Promise<Buffer>;
|
|
16
18
|
readToUint8Array(path: string): Promise<Uint8Array>;
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import type { ChecksumOptions, DirectoryListing, FileContents, GetStreamOptions, ListOptions, MimeTypeOptions, PresignedUploadPolicy, PresignedUploadPolicyOptions, PresignedUploadUrl, PresignedUploadUrlOptions, PublicUrlOptions, PutResult, PutStreamOptions, SignedUrlOptions, StatEntry, StorageAdapter, StorageAdapterConfig, TemporaryUrlOptions, Visibility } from '../types';
|
|
3
|
+
import type { S3Client } from '@stacksjs/ts-cloud';
|
|
4
4
|
/**
|
|
5
5
|
* Create an S3 storage adapter instance
|
|
6
6
|
*/
|
|
7
|
-
export declare function createS3Storage(client: S3Client, config: StorageAdapterConfig): S3StorageAdapter;
|
|
7
|
+
export declare function createS3Storage(client: S3Client | null, config: StorageAdapterConfig): S3StorageAdapter;
|
|
8
|
+
/**
|
|
9
|
+
* Append-and-take byte buffer used by the multipart pipeline
|
|
10
|
+
* (stacksjs/stacks#1886). Holds incoming chunks until they reach
|
|
11
|
+
* the configured part size, then yields them as a single Uint8Array
|
|
12
|
+
* via `take(n)` or `flush()`.
|
|
13
|
+
*/
|
|
14
|
+
declare class ChunkBuffer {
|
|
15
|
+
constructor(_partSize: number);
|
|
16
|
+
get length(): number;
|
|
17
|
+
push(c: Uint8Array): void;
|
|
18
|
+
take(n: number): Uint8Array;
|
|
19
|
+
flush(): Uint8Array;
|
|
20
|
+
}
|
|
8
21
|
/**
|
|
9
22
|
* AWS S3 storage adapter using ts-cloud S3Client
|
|
10
23
|
*/
|
|
11
24
|
export declare class S3StorageAdapter implements StorageAdapter {
|
|
12
|
-
constructor(client: S3Client, config: StorageAdapterConfig);
|
|
13
|
-
write(path: string, contents: FileContents): Promise<
|
|
25
|
+
constructor(client: S3Client | null, config: StorageAdapterConfig);
|
|
26
|
+
write(path: string, contents: FileContents): Promise<PutResult>;
|
|
14
27
|
read(path: string): Promise<FileContents>;
|
|
28
|
+
getStream(path: string, _options?: GetStreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
29
|
+
putStream(path: string, stream: ReadableStream<Uint8Array>, options?: PutStreamOptions): Promise<PutResult>;
|
|
15
30
|
readToString(path: string): Promise<string>;
|
|
16
31
|
readToBuffer(path: string): Promise<Buffer>;
|
|
17
32
|
readToUint8Array(path: string): Promise<Uint8Array>;
|
|
@@ -22,13 +37,15 @@ export declare class S3StorageAdapter implements StorageAdapter {
|
|
|
22
37
|
copyFile(from: string, to: string): Promise<void>;
|
|
23
38
|
stat(path: string): Promise<StatEntry>;
|
|
24
39
|
list(path: string, options?: ListOptions): DirectoryListing;
|
|
25
|
-
changeVisibility(
|
|
26
|
-
visibility(
|
|
40
|
+
changeVisibility(path: string, vis: Visibility): Promise<void>;
|
|
41
|
+
visibility(path: string): Promise<Visibility>;
|
|
27
42
|
fileExists(path: string): Promise<boolean>;
|
|
28
43
|
directoryExists(path: string): Promise<boolean>;
|
|
29
44
|
publicUrl(path: string, options?: PublicUrlOptions): Promise<string>;
|
|
30
45
|
temporaryUrl(path: string, options: TemporaryUrlOptions): Promise<string>;
|
|
31
46
|
signedUrl(path: string, options: SignedUrlOptions): Promise<string>;
|
|
47
|
+
presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUploadUrl>;
|
|
48
|
+
presignedUploadPolicy(options: PresignedUploadPolicyOptions): Promise<PresignedUploadPolicy>;
|
|
32
49
|
checksum(path: string, options?: ChecksumOptions): Promise<string>;
|
|
33
50
|
mimeType(path: string, _options?: MimeTypeOptions): Promise<string>;
|
|
34
51
|
lastModified(path: string): Promise<number>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ChecksumOptions, DirectoryListing, FileContents, GetStreamOptions, ListOptions, MimeTypeOptions, PresignedUploadPolicy, PresignedUploadPolicyOptions, PresignedUploadUrl, PresignedUploadUrlOptions, PublicUrlOptions, PutResult, PutStreamOptions, SignedUrlOptions, StatEntry, StorageAdapter, TemporaryUrlOptions, Visibility } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Convenience factory — most app code reads cleaner with the
|
|
4
|
+
* function form than `new ScopedStorageAdapter(...)`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const tenantDisk = scoped(Storage.disk('s3'), { scope: `tenant-${id}` })
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function scoped(inner: StorageAdapter, options: ScopedAdapterOptions): ScopedStorageAdapter;
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for a scoped adapter.
|
|
14
|
+
*/
|
|
15
|
+
export declare interface ScopedAdapterOptions {
|
|
16
|
+
scope: string
|
|
17
|
+
scopePattern?: RegExp
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wrap any `StorageAdapter` to scope every operation under a
|
|
21
|
+
* per-tenant prefix. The wrapped instance presents the same
|
|
22
|
+
* `StorageAdapter` interface so the session middleware /
|
|
23
|
+
* `Storage.disk()` / cross-disk copy all work transparently.
|
|
24
|
+
*
|
|
25
|
+
* Construction validates the scope against
|
|
26
|
+
* {@link ScopedAdapterOptions.scopePattern} so a hostile or
|
|
27
|
+
* malformed tenant id can't escape via `..` segments or path
|
|
28
|
+
* separators.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const tenantDisk = new ScopedStorageAdapter(
|
|
33
|
+
* Storage.disk('s3'),
|
|
34
|
+
* { scope: `tenant-${tenantId}` },
|
|
35
|
+
* )
|
|
36
|
+
* await tenantDisk.write('avatar.jpg', file) // → tenant-42/avatar.jpg
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class ScopedStorageAdapter implements StorageAdapter {
|
|
40
|
+
constructor(inner: StorageAdapter, options: ScopedAdapterOptions);
|
|
41
|
+
write(path: string, contents: FileContents): Promise<PutResult>;
|
|
42
|
+
read(path: string): Promise<FileContents>;
|
|
43
|
+
readToString(path: string): Promise<string>;
|
|
44
|
+
readToBuffer(path: string): Promise<Buffer>;
|
|
45
|
+
readToUint8Array(path: string): Promise<Uint8Array>;
|
|
46
|
+
deleteFile(path: string): Promise<void>;
|
|
47
|
+
deleteDirectory(path: string): Promise<void>;
|
|
48
|
+
createDirectory(path: string): Promise<void>;
|
|
49
|
+
moveFile(from: string, to: string): Promise<void>;
|
|
50
|
+
copyFile(from: string, to: string): Promise<void>;
|
|
51
|
+
stat(path: string): Promise<StatEntry>;
|
|
52
|
+
list(path: string, options?: ListOptions): DirectoryListing;
|
|
53
|
+
changeVisibility(path: string, visibility: Visibility): Promise<void>;
|
|
54
|
+
visibility(path: string): Promise<Visibility>;
|
|
55
|
+
fileExists(path: string): Promise<boolean>;
|
|
56
|
+
directoryExists(path: string): Promise<boolean>;
|
|
57
|
+
publicUrl(path: string, options?: PublicUrlOptions): Promise<string>;
|
|
58
|
+
temporaryUrl(path: string, options: TemporaryUrlOptions): Promise<string>;
|
|
59
|
+
signedUrl(path: string, options: SignedUrlOptions): Promise<string>;
|
|
60
|
+
presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUploadUrl>;
|
|
61
|
+
presignedUploadPolicy(options: PresignedUploadPolicyOptions): Promise<PresignedUploadPolicy>;
|
|
62
|
+
getStream(path: string, options?: GetStreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
63
|
+
putStream(path: string, stream: ReadableStream<Uint8Array>, options?: PutStreamOptions): Promise<PutResult>;
|
|
64
|
+
checksum(path: string, options?: ChecksumOptions): Promise<string>;
|
|
65
|
+
mimeType(path: string, options?: MimeTypeOptions): Promise<string>;
|
|
66
|
+
lastModified(path: string): Promise<number>;
|
|
67
|
+
fileSize(path: string): Promise<number>;
|
|
68
|
+
}
|
package/dist/src/facade.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { DiskConfig, FilesystemConfig, LocalDiskConfig, S3DiskConfig } from './types/filesystem';
|
|
2
|
-
import type { SignedUrlOptions, StorageAdapter } from './types';
|
|
1
|
+
import type { DiskConfig, DiskName, FilesystemConfig, LocalDiskConfig, S3DiskConfig } from './types/filesystem';
|
|
2
|
+
import type { GetStreamOptions, PresignedUploadPolicy, PresignedUploadPolicyOptions, PresignedUploadUrl, PresignedUploadUrlOptions, PutResult, PutStreamOptions, SignedUrlOptions, StatEntry, StorageAdapter } from './types';
|
|
3
|
+
import type { PutFileOptions, UploadedFileLike } from './put-file';
|
|
3
4
|
export type {
|
|
4
5
|
DiskConfig,
|
|
5
6
|
FilesystemConfig,
|
|
@@ -15,8 +16,15 @@ export declare const Storage: StorageManager;
|
|
|
15
16
|
// =============================================================================
|
|
16
17
|
declare class StorageManager {
|
|
17
18
|
init(config: Partial<FilesystemConfig>): this;
|
|
18
|
-
disk(name?:
|
|
19
|
-
put(path: string, contents: string | Uint8Array | Buffer): Promise<
|
|
19
|
+
disk(name?: DiskName): StorageAdapter;
|
|
20
|
+
put(path: string, contents: string | Uint8Array | Buffer): Promise<PutResult>;
|
|
21
|
+
put(file: UploadedFileLike, opts?: PutFileOptions): Promise<PutResult & { url: string }>;
|
|
22
|
+
put(pathOrFile: string | UploadedFileLike, contentsOrOpts?: string | Uint8Array | Buffer | PutFileOptions): Promise<PutResult | (PutResult & { url: string })>;
|
|
23
|
+
stat(path: string): Promise<StatEntry>;
|
|
24
|
+
getStream(path: string, options?: GetStreamOptions): Promise<ReadableStream<Uint8Array>>;
|
|
25
|
+
putStream(path: string, stream: ReadableStream<Uint8Array>, options?: PutStreamOptions): Promise<PutResult>;
|
|
26
|
+
copyAcross(source: string, dest: string): Promise<PutResult>;
|
|
27
|
+
moveAcross(source: string, dest: string): Promise<PutResult>;
|
|
20
28
|
get(path: string): Promise<string>;
|
|
21
29
|
exists(path: string): Promise<boolean>;
|
|
22
30
|
missing(path: string): Promise<boolean>;
|
|
@@ -25,6 +33,8 @@ declare class StorageManager {
|
|
|
25
33
|
move(from: string, to: string): Promise<void>;
|
|
26
34
|
url(path: string): Promise<string>;
|
|
27
35
|
signedUrl(path: string, options: SignedUrlOptions): Promise<string>;
|
|
36
|
+
presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUploadUrl>;
|
|
37
|
+
presignedUploadPolicy(options: PresignedUploadPolicyOptions): Promise<PresignedUploadPolicy>;
|
|
28
38
|
size(path: string): Promise<number>;
|
|
29
39
|
lastModified(path: string): Promise<number>;
|
|
30
40
|
mimeType(path: string): Promise<string>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap a Sharp pipeline so it composes with
|
|
3
|
+
* `Storage.put(file, { transform })`. The pipeline function receives a
|
|
4
|
+
* fresh Sharp instance preloaded with the file's bytes; its return
|
|
5
|
+
* value is the final pipeline chain (sharp's methods are chainable so
|
|
6
|
+
* "return img.resize(...)" is the normal shape).
|
|
7
|
+
*
|
|
8
|
+
* The wrapper handles the buffer normalisation + final `.toBuffer()`
|
|
9
|
+
* call so callers don't have to.
|
|
10
|
+
*/
|
|
11
|
+
export declare function transform(pipeline: (img: SharpInstance) => SharpInstance | Promise<SharpInstance>): (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Buffer>;
|
|
12
|
+
/**
|
|
13
|
+
* Common preset for square avatars — resizes + crops to fit, encodes
|
|
14
|
+
* as WebP at quality 85. Covers the by-far most common
|
|
15
|
+
* `Storage.put(file, { transform: ... })` callsite.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { avatar } from '@stacksjs/storage/image'
|
|
20
|
+
*
|
|
21
|
+
* await Storage.put(file, {
|
|
22
|
+
* dir: 'avatars',
|
|
23
|
+
* transform: avatar(512),
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function avatar(size?: number, quality?: number): (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Buffer>;
|
|
28
|
+
/**
|
|
29
|
+
* Generic resize preset. `fit` defaults to `'inside'` (preserve aspect
|
|
30
|
+
* ratio, don't crop) which is the most common non-avatar case.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resize(width: number, height: number, fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'): (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Buffer>;
|
|
33
|
+
/**
|
|
34
|
+
* Strip EXIF + colour profile + other embedded metadata. Useful when
|
|
35
|
+
* accepting user uploads where you don't want to leak GPS coordinates
|
|
36
|
+
* embedded in phone photos. Pairs well with resize/format presets via
|
|
37
|
+
* the explicit `transform()` form when needed.
|
|
38
|
+
*
|
|
39
|
+
* sharp strips metadata by default unless `withMetadata()` is called;
|
|
40
|
+
* this preset just makes the intent explicit.
|
|
41
|
+
*/
|
|
42
|
+
export declare function stripMetadata(): (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Buffer>;
|
|
43
|
+
declare interface SharpInstance {
|
|
44
|
+
toBuffer(): Promise<Buffer>
|
|
45
|
+
resize: (...args: unknown[]) => SharpInstance
|
|
46
|
+
jpeg: (...args: unknown[]) => SharpInstance
|
|
47
|
+
png: (...args: unknown[]) => SharpInstance
|
|
48
|
+
webp: (...args: unknown[]) => SharpInstance
|
|
49
|
+
avif: (...args: unknown[]) => SharpInstance
|
|
50
|
+
gif: (...args: unknown[]) => SharpInstance
|
|
51
|
+
rotate: (...args: unknown[]) => SharpInstance
|
|
52
|
+
blur: (...args: unknown[]) => SharpInstance
|
|
53
|
+
withMetadata: (...args: unknown[]) => SharpInstance
|
|
54
|
+
[k: string]: unknown
|
|
55
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
export type { ServeFileOptions } from './static-serve';
|
|
2
2
|
export type { DiskConfig, FilesystemConfig, LocalDiskConfig, S3DiskConfig } from './facade';
|
|
3
|
+
export type { FilenameStrategy, PutFileOptions, UploadedFileLike } from './put-file';
|
|
4
|
+
// Disk-name autocomplete: userland augments `KnownDisks` to get
|
|
5
|
+
// completion on `Storage.disk('…')` (stacksjs/stacks#1924).
|
|
6
|
+
export type { DiskName, KnownDisks } from './types/filesystem';
|
|
3
7
|
export type { SignedTokenVerification } from './signed-url';
|
|
4
8
|
export type { SignedUrlOptions } from './types';
|
|
9
|
+
export type { ParsedDiskPath } from './path-sanitize';
|
|
10
|
+
export type { MimeVerifyResult } from './mime-verify';
|
|
11
|
+
export type { S3PresignedPostInput, S3PresignedPostResult } from './s3-presigned-post';
|
|
12
|
+
export type {
|
|
13
|
+
GetStreamOptions,
|
|
14
|
+
PresignedUploadPolicy,
|
|
15
|
+
PresignedUploadPolicyOptions,
|
|
16
|
+
PutResult,
|
|
17
|
+
PutStreamOptions,
|
|
18
|
+
} from './types';
|
|
5
19
|
export * from './copy';
|
|
6
20
|
export * from './delete';
|
|
7
21
|
export * from './files';
|
|
@@ -24,4 +38,23 @@ export { UploadedFile, uploadedFile, uploadedFiles } from './uploaded-file';
|
|
|
24
38
|
// Filesystem type helpers
|
|
25
39
|
export { configFromEnv, localDisk, s3Disk } from './types/filesystem';
|
|
26
40
|
// Signed-URL helpers (used by Storage.disk('local').signedUrl())
|
|
27
|
-
export {
|
|
41
|
+
export {
|
|
42
|
+
clearRevokedSignedStorageTokens,
|
|
43
|
+
createSignedStorageToken,
|
|
44
|
+
isSignedStorageTokenRevoked,
|
|
45
|
+
revokeSignedStorageToken,
|
|
46
|
+
verifySignedStorageToken,
|
|
47
|
+
} from './signed-url';
|
|
48
|
+
// Path-sanitization helpers (used internally by S3 presignedUploadUrl;
|
|
49
|
+
// also re-exported so user code can pre-validate input from request
|
|
50
|
+
// bodies and surface 400s with a clean `PathSanitizeError`.)
|
|
51
|
+
export { parseDiskPath, PathSanitizeError, sanitizePresignedDir, sanitizePresignedFilename } from './path-sanitize';
|
|
52
|
+
// MIME re-verification — server-side check that uploaded bytes match
|
|
53
|
+
// the claimed content type. Run this after a presigned upload
|
|
54
|
+
// completes since `Content-Type` on a presigned PUT is caller-attested.
|
|
55
|
+
export { detectMimeFromMagicBytes, verifyUploadedMime } from './mime-verify';
|
|
56
|
+
// S3 presigned-POST policy signer (stacksjs/stacks#1888 Phase B).
|
|
57
|
+
// Exposed as a standalone for callers that have their own S3 client
|
|
58
|
+
// and just need the policy-signing logic; the Storage facade wraps
|
|
59
|
+
// it as `presignedUploadPolicy()`.
|
|
60
|
+
export { signS3PresignedPost } from './s3-presigned-post';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect a MIME type from the first chunk of a file. Returns the
|
|
3
|
+
* detected MIME (e.g. `'image/png'`) or `null` if the bytes don't
|
|
4
|
+
* match any known signature.
|
|
5
|
+
*
|
|
6
|
+
* Intentionally narrow — only the well-known binary formats that
|
|
7
|
+
* appear in `presignedUploadUrl`'s extension map are detected. Text
|
|
8
|
+
* formats are not covered because their signatures are ambiguous.
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectMimeFromMagicBytes(bytes: Uint8Array | ArrayBuffer): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Verify that a file's actual contents match the claimed content type.
|
|
13
|
+
* Returns `{ ok, expected, detected }` so callers can branch on the
|
|
14
|
+
* result and produce useful error messages.
|
|
15
|
+
*
|
|
16
|
+
* Reads up to 32 bytes from the file (enough for every signature we
|
|
17
|
+
* check), then matches against `detectMimeFromMagicBytes`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* // After a presigned upload completes:
|
|
22
|
+
* const result = await verifyUploadedMime('uploads/avatar.jpg', 'image/jpeg')
|
|
23
|
+
* if (!result.ok) {
|
|
24
|
+
* await Storage.disk().deleteFile('uploads/avatar.jpg')
|
|
25
|
+
* return Response.json({ error: 'content type mismatch', ...result }, { status: 400 })
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyUploadedMime(path: string, expectedContentType: string, options?: { disk?: string }): Promise<MimeVerifyResult>;
|
|
30
|
+
/**
|
|
31
|
+
* MIME re-verification helpers (stacksjs/stacks#1873 S-3).
|
|
32
|
+
*
|
|
33
|
+
* Background: `presignedUploadUrl({ contentType })` lets the caller
|
|
34
|
+
* declare what they're going to upload, and AWS signs the URL against
|
|
35
|
+
* that exact `Content-Type` header. Nothing checks that the **bytes**
|
|
36
|
+
* actually match the claim. An attacker who can call your presigned
|
|
37
|
+
* endpoint can request `image/jpeg` (which derives a `.jpg`
|
|
38
|
+
* extension), then PUT a JavaScript file. The server only sees
|
|
39
|
+
* "object exists, contentType was image/jpeg" — but the bytes are
|
|
40
|
+
* executable.
|
|
41
|
+
*
|
|
42
|
+
* These helpers exist so server code can re-detect the MIME from
|
|
43
|
+
* magic bytes after the upload finishes, and either delete the
|
|
44
|
+
* mismatched object or surface it as a 400.
|
|
45
|
+
*
|
|
46
|
+
* **Limitations** — magic-byte sniffing only works for binary formats
|
|
47
|
+
* with a well-defined signature. Text-based types (JSON, CSV, plain
|
|
48
|
+
* text, SVG, HTML) can't be unambiguously detected from the first few
|
|
49
|
+
* bytes; for those, validate by parsing the content (e.g. try
|
|
50
|
+
* `JSON.parse` for `application/json`).
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Result of a magic-byte detection attempt.
|
|
54
|
+
*
|
|
55
|
+
* `ok: true` means the bytes match a known signature for the
|
|
56
|
+
* expected content type. `ok: false` with `detected: null` means the
|
|
57
|
+
* bytes didn't match any signature this helper knows; `ok: false`
|
|
58
|
+
* with `detected: string` means the bytes match a *different*
|
|
59
|
+
* signature than expected (e.g. PNG bytes uploaded as image/jpeg).
|
|
60
|
+
*/
|
|
61
|
+
export declare interface MimeVerifyResult {
|
|
62
|
+
ok: boolean
|
|
63
|
+
expected: string
|
|
64
|
+
detected: string | null
|
|
65
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a `dir` parameter for `presignedUploadUrl`. Returns the
|
|
3
|
+
* cleaned dir on success (trailing slashes stripped, empty string
|
|
4
|
+
* preserved as-is) or throws `PathSanitizeError` on:
|
|
5
|
+
*
|
|
6
|
+
* - non-string input
|
|
7
|
+
* - absolute paths (`/foo`)
|
|
8
|
+
* - traversal segments (`..`, `foo/../bar`)
|
|
9
|
+
* - null bytes (`foo\0bar`) — these get stripped by some S3 SDKs
|
|
10
|
+
* silently
|
|
11
|
+
* - control characters (`\r`, `\n`, etc.) — log-injection risk
|
|
12
|
+
* - segments outside `[A-Za-z0-9._-]`
|
|
13
|
+
*
|
|
14
|
+
* Empty / undefined dir is allowed (it means "write at the root of
|
|
15
|
+
* the configured prefix").
|
|
16
|
+
*/
|
|
17
|
+
export declare function sanitizePresignedDir(dir: string | undefined): string;
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize a `filename` parameter for `presignedUploadUrl`. Returns
|
|
20
|
+
* the cleaned filename on success or throws `PathSanitizeError`.
|
|
21
|
+
*
|
|
22
|
+
* Rejects path separators, traversal tokens, null bytes, control
|
|
23
|
+
* characters, and disallowed characters. Validates the extension
|
|
24
|
+
* against a strict alphanumeric pattern (no `.exe.jpg` smuggling —
|
|
25
|
+
* the caller is responsible for matching extension to expected
|
|
26
|
+
* content type via the contentType the URL was signed for).
|
|
27
|
+
*/
|
|
28
|
+
export declare function sanitizePresignedFilename(filename: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Parse a `disk:path` reference used by `Storage.copyAcross()` /
|
|
31
|
+
* `moveAcross()` (stacksjs/stacks#1888 S-7).
|
|
32
|
+
*
|
|
33
|
+
* Format: `<disk>:<path>` where:
|
|
34
|
+
* - `<disk>` is an alphanumeric + dash/underscore disk name
|
|
35
|
+
* - `<path>` is a storage-relative path (path-traversal /
|
|
36
|
+
* null-byte / control-char checks applied)
|
|
37
|
+
*
|
|
38
|
+
* Throws {@link PathSanitizeError} on a malformed input — the
|
|
39
|
+
* cross-disk helpers turn that into a clear "bad source" / "bad
|
|
40
|
+
* dest" error rather than crashing inside the adapter.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* parseDiskPath('s3:user-uploads/foo.jpg')
|
|
45
|
+
* // → { disk: 's3', path: 'user-uploads/foo.jpg' }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseDiskPath(input: string): ParsedDiskPath;
|
|
49
|
+
/**
|
|
50
|
+
* Parsed `disk:path` reference returned by {@link parseDiskPath}.
|
|
51
|
+
*/
|
|
52
|
+
export declare interface ParsedDiskPath {
|
|
53
|
+
disk: string
|
|
54
|
+
path: string
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Path-sanitization helpers for storage adapters (stacksjs/stacks#1873).
|
|
58
|
+
*
|
|
59
|
+
* Callers of `presignedUploadUrl({ dir, filename })` pass in
|
|
60
|
+
* caller-controlled strings that get interpolated straight into the
|
|
61
|
+
* stored key. Without sanitization, `dir: '../../sensitive'` escapes
|
|
62
|
+
* the intended prefix and `filename: 'foo/bar.exe'` injects a
|
|
63
|
+
* directory separator — both let a hostile caller (or a confused
|
|
64
|
+
* authenticated caller) write to objects outside their intended
|
|
65
|
+
* scope. These helpers reject the dangerous shapes loudly before the
|
|
66
|
+
* adapter ever signs anything.
|
|
67
|
+
*
|
|
68
|
+
* Note: the local/bun adapters already have a defense-in-depth check
|
|
69
|
+
* via `path.relative()` in `resolvePath()`. The S3 adapter doesn't,
|
|
70
|
+
* because S3 keys are opaque strings — there's no filesystem `..`
|
|
71
|
+
* resolution to lean on. That's exactly why we need this layer.
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* Thrown when `sanitizePresignedDir` or `sanitizePresignedFilename`
|
|
75
|
+
* detects a value that would escape the intended scope. The `reason`
|
|
76
|
+
* discriminant lets callers distinguish "you passed an absolute path"
|
|
77
|
+
* from "you passed a null byte" if they want to surface that in error
|
|
78
|
+
* messages — most callers can just `catch (e: PathSanitizeError)` and
|
|
79
|
+
* return a 400.
|
|
80
|
+
*/
|
|
81
|
+
export declare class PathSanitizeError extends Error {
|
|
82
|
+
readonly reason: | 'empty'
|
|
83
|
+
| 'not-string'
|
|
84
|
+
| 'absolute-path'
|
|
85
|
+
| 'traversal'
|
|
86
|
+
| 'null-byte'
|
|
87
|
+
| 'control-char'
|
|
88
|
+
| 'too-long'
|
|
89
|
+
| 'invalid-char'
|
|
90
|
+
| 'invalid-extension';
|
|
91
|
+
constructor(message: string, reason: PathSanitizeError['reason']);
|
|
92
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DiskName } from './types/filesystem';
|
|
2
|
+
import type { PutResult } from './types';
|
|
3
|
+
import type { StorageManager } from './facade';
|
|
4
|
+
export declare function putUploadedFile(manager: StorageManager, file: UploadedFileLike, opts: PutFileOptions): Promise<PutResult & { url: string }>;
|
|
5
|
+
/**
|
|
6
|
+
* Optional metadata fields shared by every uploaded-file shape we
|
|
7
|
+
* accept. The `name` / `mimeType` aliases are present so the router's
|
|
8
|
+
* `UploadedFile` class (which uses the class-style names) flows
|
|
9
|
+
* through alongside the direct-parse shape (which uses the
|
|
10
|
+
* snake-style `originalName` / `mimetype`).
|
|
11
|
+
*/
|
|
12
|
+
declare interface UploadedFileMetadata {
|
|
13
|
+
originalName?: string
|
|
14
|
+
name?: string
|
|
15
|
+
mimetype?: string
|
|
16
|
+
mimeType?: string
|
|
17
|
+
}
|
|
18
|
+
export declare interface PutFileOptions {
|
|
19
|
+
disk?: DiskName
|
|
20
|
+
dir?: string
|
|
21
|
+
filename?: FilenameStrategy
|
|
22
|
+
preserveExtension?: boolean
|
|
23
|
+
transform?: (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Uint8Array | Buffer>
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Minimal structural shape for an uploaded file accepted by
|
|
27
|
+
* `Storage.put(file, opts)`. Modeled as a discriminated union so the
|
|
28
|
+
* type-checker rejects `Storage.put({})` and similar empty-object
|
|
29
|
+
* mistakes (stacksjs/stacks#1873 S-13). At least one of `buffer`,
|
|
30
|
+
* `bytes()`, or `arrayBuffer()` must be present — that's the runtime
|
|
31
|
+
* contract `readBytes()` enforces with a throw, and now the
|
|
32
|
+
* structural contract the type system enforces at compile time.
|
|
33
|
+
*
|
|
34
|
+
* Two callsites land here in practice (stacksjs/stacks#1856):
|
|
35
|
+
*
|
|
36
|
+
* 1. **Direct multipart parse** (the original router shape, before
|
|
37
|
+
* bun-router wrapped each entry in an `UploadedFile` class).
|
|
38
|
+
* `{ originalName, mimetype, buffer }` — synchronous.
|
|
39
|
+
* 2. **Router's `UploadedFile` class** (current shape from
|
|
40
|
+
* `req.file(key)` / `req.files`). Exposes `name`, `mimeType`, and
|
|
41
|
+
* an async `bytes()` / `arrayBuffer()` accessor instead of a
|
|
42
|
+
* `buffer` property — Bun's `File` is lazy by design.
|
|
43
|
+
*/
|
|
44
|
+
export type UploadedFileLike = UploadedFileMetadata & (
|
|
45
|
+
| { buffer: ArrayBuffer | Uint8Array | Buffer, bytes?: () => Promise<Uint8Array>, arrayBuffer?: () => Promise<ArrayBuffer> }
|
|
46
|
+
| { bytes: () => Promise<Uint8Array>, buffer?: ArrayBuffer | Uint8Array | Buffer, arrayBuffer?: () => Promise<ArrayBuffer> }
|
|
47
|
+
| { arrayBuffer: () => Promise<ArrayBuffer>, buffer?: ArrayBuffer | Uint8Array | Buffer, bytes?: () => Promise<Uint8Array> }
|
|
48
|
+
);
|
|
49
|
+
/** Built-in filename strategies for `Storage.put(file, { filename })`. */
|
|
50
|
+
export type FilenameStrategy = | 'uuid'
|
|
51
|
+
| 'hash'
|
|
52
|
+
| 'original'
|
|
53
|
+
| ((file: UploadedFileLike) => string);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build + sign an S3 presigned-POST policy.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* const post = signS3PresignedPost({
|
|
7
|
+
* bucket: 'app-uploads',
|
|
8
|
+
* region: 'us-east-1',
|
|
9
|
+
* credentials: { accessKeyId, secretAccessKey },
|
|
10
|
+
* key: { startsWith: 'avatars/' },
|
|
11
|
+
* contentType: { startsWith: 'image/' },
|
|
12
|
+
* contentLengthRange: { min: 0, max: 5 * 1024 * 1024 },
|
|
13
|
+
* expiresIn: 3600,
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* // Browser side:
|
|
17
|
+
* // const fd = new FormData()
|
|
18
|
+
* // Object.entries(post.fields).forEach(([k, v]) => fd.append(k, v))
|
|
19
|
+
* // fd.append('file', file) // MUST be last
|
|
20
|
+
* // await fetch(post.url, { method: 'POST', body: fd })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function signS3PresignedPost(input: S3PresignedPostInput): S3PresignedPostResult;
|
|
24
|
+
/**
|
|
25
|
+
* Inputs to {@link signS3PresignedPost}. Mirrors the
|
|
26
|
+
* `presignedUploadPolicy()` adapter call once it's wired up.
|
|
27
|
+
*/
|
|
28
|
+
export declare interface S3PresignedPostInput {
|
|
29
|
+
bucket: string
|
|
30
|
+
region: string
|
|
31
|
+
credentials: {
|
|
32
|
+
accessKeyId: string
|
|
33
|
+
secretAccessKey: string
|
|
34
|
+
sessionToken?: string
|
|
35
|
+
}
|
|
36
|
+
key: string | { startsWith: string }
|
|
37
|
+
contentType: string | { startsWith: string }
|
|
38
|
+
contentLengthRange?: { min: number, max: number }
|
|
39
|
+
acl?: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read' | 'bucket-owner-read' | 'bucket-owner-full-control'
|
|
40
|
+
expiresIn: number
|
|
41
|
+
fields?: Record<string, string>
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What the browser submits. The form is `multipart/form-data` POSTed
|
|
45
|
+
* to `url`; every key in `fields` becomes a form field with the same
|
|
46
|
+
* name. The actual file MUST be the LAST field, named `'file'`.
|
|
47
|
+
*/
|
|
48
|
+
export declare interface S3PresignedPostResult {
|
|
49
|
+
url: string
|
|
50
|
+
fields: Record<string, string>
|
|
51
|
+
key: string
|
|
52
|
+
}
|