@stacksjs/storage 0.70.86 → 0.70.88

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.
@@ -1,65 +0,0 @@
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
- }
package/dist/move.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import type { Result } from '@stacksjs/error-handling';
2
- export declare function move(src: string | string[], dest: string, options?: MoveOptions): Promise<Result<{ message: string }, Error>>;
3
- export declare function rename(from: string, to: string, options?: MoveOptions): Promise<Result<{ message: string }, Error>>;
4
- declare interface MoveOptions {
5
- overwrite?: boolean
6
- }
@@ -1,92 +0,0 @@
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
- }
@@ -1,53 +0,0 @@
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);
@@ -1,52 +0,0 @@
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
- }
@@ -1,69 +0,0 @@
1
- import type { SignedUrlOptions } from './types';
2
- /**
3
- * Mint a signed token for the given storage path.
4
- *
5
- * @example
6
- * ```ts
7
- * const token = createSignedStorageToken('reports/q4.pdf', { expiresIn: 3600 })
8
- * ```
9
- */
10
- export declare function createSignedStorageToken(path: string, options: SignedUrlOptions): string;
11
- /**
12
- * Revoke a signed storage token so subsequent
13
- * {@link verifySignedStorageToken} calls return
14
- * `{ valid: false, reason: 'revoked' }`. Idempotent — calling
15
- * twice is a no-op.
16
- *
17
- * Pass either the full JWS compact-form token or just the signature
18
- * segment (the part after the second `.`); both work because
19
- * verification keys off the signature segment.
20
- *
21
- * @example
22
- * ```ts
23
- * const url = await Storage.disk('local').signedUrl('reports/q4.pdf', { expiresIn: 3600 })
24
- * // ... url is shared, then later leaked
25
- * revokeSignedStorageToken(extractTokenFromUrl(url))
26
- * // Any further fetch with that URL → 403
27
- * ```
28
- */
29
- export declare function revokeSignedStorageToken(token: string): void;
30
- /**
31
- * Check whether a signature has been revoked. Exposed for tests
32
- * and for distributed-cache replicators that need to peek at the
33
- * set; production callers should rely on {@link verifySignedStorageToken}
34
- * to consult this automatically.
35
- */
36
- export declare function isSignedStorageTokenRevoked(sigPart: string): boolean;
37
- /**
38
- * Test-only: clear the revocation set. The set is process-local
39
- * and unbounded across tests would let one test's revoke bleed
40
- * into another's verification.
41
- */
42
- export declare function clearRevokedSignedStorageTokens(): void;
43
- /**
44
- * Verify a signed storage token. The caller MUST pass the requested
45
- * path so we can ensure the token's `path` claim matches what the
46
- * client is trying to fetch — otherwise an attacker could substitute
47
- * any path in the URL and still pass signature verification.
48
- *
49
- * @example
50
- * ```ts
51
- * const v = verifySignedStorageToken(req.query.token, requestedPath)
52
- * if (!v.valid) return new Response('Forbidden', { status: 403 })
53
- * ```
54
- */
55
- export declare function verifySignedStorageToken(token: string, requestedPath: string): SignedTokenVerification;
56
- declare interface SignedTokenClaims {
57
- iss: string
58
- iat: number
59
- exp: number
60
- path: string
61
- }
62
- /**
63
- * Result of verifying a signed token.
64
- */
65
- export declare interface SignedTokenVerification {
66
- valid: boolean
67
- reason?: 'malformed' | 'bad_signature' | 'expired' | 'path_mismatch' | 'revoked'
68
- claims?: SignedTokenClaims
69
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Serve a static file with production-grade cache headers:
3
- * - Strong ETag (sha256, first 16 hex chars) computed once per
4
- * `(path, mtime)` and cached in-process.
5
- * - `Last-Modified` from the file's mtime.
6
- * - 304 Not Modified when `If-None-Match` matches OR
7
- * `If-Modified-Since` is at-or-after mtime (and ETag didn't mismatch).
8
- * - `Cache-Control: public, max-age=31536000, immutable` for paths
9
- * that look fingerprinted (e.g. `/_assets/foo.abc12345.js`).
10
- * - `Cache-Control: public, max-age=300, must-revalidate` for
11
- * everything else.
12
- *
13
- * Returns a `404` if the file doesn't exist. Other read errors propagate.
14
- *
15
- * @example
16
- * ```ts
17
- * import { serveFile } from '@stacksjs/storage'
18
- * import { resolve } from 'node:path'
19
- *
20
- * route.get('/assets/:path', async (req) => {
21
- * const url = new URL(req.url)
22
- * // url.pathname is e.g. /assets/app.abc12345.js
23
- * const filePath = resolve('./public', url.pathname.replace(/^\//, ''))
24
- * return serveFile(req, filePath)
25
- * })
26
- * ```
27
- */
28
- export declare function serveFile(req: Request, filePath: string, options?: ServeFileOptions): Promise<Response>;
29
- /**
30
- * Optional knobs for {@link serveFile}.
31
- */
32
- export declare interface ServeFileOptions {
33
- contentType?: string
34
- cacheControl?: string
35
- defaultMaxAge?: number
36
- etag?: boolean
37
- }
package/dist/storage.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export * from './copy';
2
- export * from './delete';
3
- export * from './files';
4
- export * from './folders';
5
- export * from './fs';
6
- export * from './helpers';
7
- export * from './move';
8
- export * from './visibility';
9
- export * from './zip';
@@ -1,131 +0,0 @@
1
- /**
2
- * Helper to create a local disk config
3
- */
4
- export declare function localDisk(root: string, options?: Partial<Omit<LocalDiskConfig, 'driver' | 'root'>>): LocalDiskConfig;
5
- /**
6
- * Helper to create an S3 disk config
7
- */
8
- export declare function s3Disk(bucket: string, options?: Partial<Omit<S3DiskConfig, 'driver' | 'bucket'>>): S3DiskConfig;
9
- /**
10
- * Create filesystem config from environment variables
11
- */
12
- export declare function configFromEnv(base?: Partial<FilesystemConfig>): FilesystemConfig;
13
- /**
14
- * Base disk configuration shared by all drivers
15
- */
16
- declare interface BaseDiskConfig {
17
- name?: string
18
- visibility?: Visibility
19
- throw?: boolean
20
- }
21
- /**
22
- * Local filesystem disk configuration
23
- */
24
- export declare interface LocalDiskConfig extends BaseDiskConfig {
25
- driver: 'local'
26
- root: string
27
- url?: string
28
- }
29
- /**
30
- * S3 disk configuration
31
- */
32
- export declare interface S3DiskConfig extends BaseDiskConfig {
33
- driver: 's3'
34
- bucket: string
35
- region?: string
36
- prefix?: string
37
- endpoint?: string
38
- usePathStyleEndpoint?: boolean
39
- url?: string
40
- credentials?: {
41
- key: string
42
- secret: string
43
- }
44
- }
45
- /**
46
- * Userland-augmentable disk-name registry (stacksjs/stacks#1924).
47
- *
48
- * Empty by default — the framework can't know an app's configured
49
- * disks at its own build time. Apps declare their disks once and get
50
- * autocomplete on `Storage.disk('…')` everywhere:
51
- *
52
- * ```ts
53
- * // types/storage.d.ts
54
- * declare module '@stacksjs/storage' {
55
- * interface KnownDisks {
56
- * local: true
57
- * public: true
58
- * s3: true
59
- * }
60
- * }
61
- * ```
62
- *
63
- * Mirrors the `DatabaseSchema` pattern from stacksjs/stacks#1923.
64
- */
65
- // eslint-disable-next-line ts/no-empty-object-type
66
- export declare interface KnownDisks {}
67
- /**
68
- * Main filesystem configuration
69
- *
70
- * @example
71
- * ```ts
72
- * const config: FilesystemConfig = {
73
- * default: 'local',
74
- * disks: {
75
- * local: {
76
- * driver: 'local',
77
- * root: '/storage/app',
78
- * },
79
- * public: {
80
- * driver: 'local',
81
- * root: '/public',
82
- * url: '/storage',
83
- * visibility: 'public',
84
- * },
85
- * s3: {
86
- * driver: 's3',
87
- * bucket: 'my-bucket',
88
- * region: 'us-east-1',
89
- * },
90
- * },
91
- * }
92
- * ```
93
- */
94
- export declare interface FilesystemConfig {
95
- default: string
96
- disks: Record<string, DiskConfig>
97
- }
98
- /**
99
- * Environment variable mappings for filesystem configuration
100
- */
101
- export declare interface FilesystemEnv {
102
- FILESYSTEM_DISK?: string
103
- AWS_ACCESS_KEY_ID?: string
104
- AWS_SECRET_ACCESS_KEY?: string
105
- AWS_DEFAULT_REGION?: string
106
- AWS_BUCKET?: string
107
- AWS_ENDPOINT?: string
108
- AWS_URL?: string
109
- AWS_USE_PATH_STYLE_ENDPOINT?: string
110
- }
111
- /**
112
- * Filesystem Configuration Types
113
- *
114
- * Laravel-style filesystem configuration with clean, typed interfaces.
115
- * Supports local, public, and S3 disk drivers.
116
- */
117
- export type FilesystemDriver = 'local' | 's3';
118
- export type Visibility = 'public' | 'private';
119
- /**
120
- * Union type for all disk configurations
121
- */
122
- export type DiskConfig = LocalDiskConfig | S3DiskConfig;
123
- /**
124
- * A configured disk name (autocompletes to the keys of an augmented
125
- * {@link KnownDisks}) or any other string. The `(string & {})` branch
126
- * keeps the union from collapsing back to `string`, so known disks
127
- * surface in autocomplete while arbitrary names still type-check —
128
- * apps that haven't augmented `KnownDisks` keep compiling unchanged.
129
- */
130
- // eslint-disable-next-line ts/no-empty-object-type
131
- export type DiskName = (keyof KnownDisks & string) | (string & {});