@stacksjs/storage 0.70.161 → 0.70.163

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,10 +1,33 @@
1
1
  import { Buffer } from 'node:buffer';
2
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
3
  import type { S3Client } from '@stacksjs/ts-cloud';
4
+ /**
5
+ * Build ts-cloud `S3ClientOptions` from adapter config, or `undefined` for a
6
+ * plain AWS S3 client. The config endpoint may carry a scheme
7
+ * (`https://s3.filebase.com`); ts-cloud wants a scheme-less host, so it's
8
+ * stripped here. This is what routes the adapter to S3-compatible providers -
9
+ * Filebase, Backblaze B2, Cloudflare R2, Hetzner Object Storage
10
+ * (stacksjs/stacks#938, #1897, #1896).
11
+ */
12
+ export declare function resolveS3ClientOptions(config: {
13
+ endpoint?: string
14
+ usePathStyleEndpoint?: boolean
15
+ credentials?: { accessKeyId: string, secretAccessKey: string, sessionToken?: string }
16
+ }): S3ClientOptions | undefined;
4
17
  /**
5
18
  * Create an S3 storage adapter instance
6
19
  */
7
20
  export declare function createS3Storage(client: S3Client | null, config: StorageAdapterConfig): S3StorageAdapter;
21
+ /**
22
+ * Options for the ts-cloud S3 client. Mirrors ts-cloud's internal
23
+ * `S3ClientOptions` (declared but not re-exported from its package root);
24
+ * structural typing lets this satisfy the `S3Client` constructor's 3rd argument.
25
+ */
26
+ declare interface S3ClientOptions {
27
+ endpoint?: string
28
+ forcePathStyle?: boolean
29
+ credentials?: { accessKeyId: string, secretAccessKey: string, sessionToken?: string }
30
+ }
8
31
  /**
9
32
  * Append-and-take byte buffer used by the multipart pipeline
10
33
  * (stacksjs/stacks#1886). Holds incoming chunks until they reach
@@ -59,6 +59,16 @@ async function isSettled(p) {
59
59
  Promise.resolve(sentinel)
60
60
  ]) !== sentinel;
61
61
  }
62
+ export function resolveS3ClientOptions(config) {
63
+ const options = {};
64
+ if (config.endpoint)
65
+ options.endpoint = config.endpoint.replace(/^https?:\/\//i, "").replace(/\/+$/, "");
66
+ if (config.usePathStyleEndpoint)
67
+ options.forcePathStyle = !0;
68
+ if (config.credentials)
69
+ options.credentials = config.credentials;
70
+ return Object.keys(options).length > 0 ? options : void 0;
71
+ }
62
72
 
63
73
  export class S3StorageAdapter {
64
74
  _client;
@@ -66,6 +76,8 @@ export class S3StorageAdapter {
66
76
  bucket;
67
77
  prefix;
68
78
  region;
79
+ endpoint;
80
+ usePathStyleEndpoint;
69
81
  credentials;
70
82
  constructor(client, config) {
71
83
  this._client = client;
@@ -73,6 +85,8 @@ export class S3StorageAdapter {
73
85
  this.prefix = config.prefix || "";
74
86
  this.region = config.region || "us-east-1";
75
87
  this.credentials = config.credentials;
88
+ this.endpoint = config.endpoint;
89
+ this.usePathStyleEndpoint = config.usePathStyleEndpoint;
76
90
  if (!this.bucket)
77
91
  throw Error("S3 bucket name is required");
78
92
  }
@@ -81,7 +95,7 @@ export class S3StorageAdapter {
81
95
  return this._client;
82
96
  if (!this._clientPromise)
83
97
  this._clientPromise = import("@stacksjs/ts-cloud").then((cloud) => {
84
- this._client = new cloud.S3Client(this.region);
98
+ this._client = new cloud.S3Client(this.region, void 0, resolveS3ClientOptions({ endpoint: this.endpoint, usePathStyleEndpoint: this.usePathStyleEndpoint, credentials: this.credentials }));
85
99
  return this._client;
86
100
  });
87
101
  return this._clientPromise;
package/dist/facade.js CHANGED
@@ -87,7 +87,10 @@ class StorageManager {
87
87
  return new S3StorageAdapter(null, {
88
88
  bucket: config.bucket,
89
89
  region: config.region,
90
- prefix: config.prefix
90
+ prefix: config.prefix,
91
+ endpoint: config.endpoint,
92
+ usePathStyleEndpoint: config.usePathStyleEndpoint,
93
+ credentials: config.credentials ? { accessKeyId: config.credentials.key, secretAccessKey: config.credentials.secret } : void 0
91
94
  });
92
95
  }
93
96
  async put(pathOrFile, contentsOrOpts) {
package/dist/image.d.ts CHANGED
@@ -1,55 +1,31 @@
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
- */
1
+ import type { ResizeFit } from 'ts-images';
2
+ export declare function transform(pipeline: (image: NativeImagePipeline) => NativeImagePipeline | Promise<NativeImagePipeline>): (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Buffer>;
27
3
  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
4
+ export declare function resize(width: number, height: number, fit?: ResizeFit): (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Buffer>;
5
+ export declare function stripMetadata(format?: NativeImageFormat): (input: Uint8Array | Buffer | ArrayBuffer) => Promise<Buffer>;
6
+ export declare interface NativeEncodeOptions { quality?: number, progressive?: boolean, lossless?: boolean, effort?: number }
7
+ export declare interface NativeResizeOptions { fit?: ResizeFit, withoutEnlargement?: boolean }
8
+ export declare interface NativeImagePipeline {
9
+ resize: (width?: number, height?: number, options?: NativeResizeOptions) => NativeImagePipeline
10
+ jpeg: (options?: NativeEncodeOptions) => NativeImagePipeline
11
+ png: (options?: NativeEncodeOptions) => NativeImagePipeline
12
+ webp: (options?: NativeEncodeOptions) => NativeImagePipeline
13
+ avif: (options?: NativeEncodeOptions) => NativeImagePipeline
14
+ rotate: (degrees?: number) => NativeImagePipeline
15
+ blur: (sigma?: number) => NativeImagePipeline
16
+ withMetadata: () => NativeImagePipeline
17
+ toBuffer: () => Promise<Buffer>
18
+ }
19
+ export type NativeImageFormat = 'jpeg' | 'png' | 'webp' | 'avif';
20
+ declare class Pipeline implements NativeImagePipeline {
21
+ constructor(input: Uint8Array);
22
+ resize(width?: number, height?: number, options?: NativeResizeOptions): NativeImagePipeline;
23
+ jpeg(options?: NativeEncodeOptions): NativeImagePipeline;
24
+ png(options?: NativeEncodeOptions): NativeImagePipeline;
25
+ webp(options?: NativeEncodeOptions): NativeImagePipeline;
26
+ avif(options?: NativeEncodeOptions): NativeImagePipeline;
27
+ rotate(degrees?: number): NativeImagePipeline;
28
+ blur(sigma?: number): NativeImagePipeline;
29
+ withMetadata(): NativeImagePipeline;
30
+ toBuffer(): Promise<Buffer>;
55
31
  }
package/dist/image.js CHANGED
@@ -1,29 +1,81 @@
1
- let cachedSharp = null;
2
- async function loadSharp() {
3
- if (cachedSharp)
4
- return cachedSharp;
5
- try {
6
- const mod = await import("sharp");
7
- cachedSharp = mod.default ?? mod;
8
- return cachedSharp;
9
- } catch (err) {
10
- throw Error(`[storage/image] \`sharp\` is not installed. Image transforms require it as a peer dependency.
11
- Install with: \`bun add sharp\`
12
- Original load error: ${err instanceof Error ? err.message : String(err)}`);
1
+ import { blur as applyBlur, decode, encode, resize as applyResize, rotate as applyRotate } from "ts-images";
2
+
3
+ class Pipeline {
4
+ input;
5
+ operations = [];
6
+ format = "webp";
7
+ options = { quality: 80 };
8
+ constructor(input) {
9
+ this.input = input;
10
+ }
11
+ resize(width, height, options = {}) {
12
+ if (width !== void 0 && (!Number.isInteger(width) || width <= 0))
13
+ throw TypeError("Image width must be positive");
14
+ if (height !== void 0 && (!Number.isInteger(height) || height <= 0))
15
+ throw TypeError("Image height must be positive");
16
+ if (width === void 0 && height === void 0)
17
+ throw TypeError("Image resize requires a width or height");
18
+ this.operations.push((image) => {
19
+ if (options.withoutEnlargement !== !1 && (width === void 0 || width >= image.width) && (height === void 0 || height >= image.height))
20
+ return image;
21
+ return applyResize(image, { width, height, fit: options.fit ?? "inside" });
22
+ });
23
+ return this;
24
+ }
25
+ jpeg(options = {}) {
26
+ return this.output("jpeg", options);
27
+ }
28
+ png(options = {}) {
29
+ return this.output("png", options);
30
+ }
31
+ webp(options = {}) {
32
+ return this.output("webp", options);
33
+ }
34
+ avif(options = {}) {
35
+ return this.output("avif", options);
36
+ }
37
+ rotate(degrees = 90) {
38
+ if (!Number.isFinite(degrees))
39
+ throw TypeError("Image rotation must be finite");
40
+ this.operations.push((image) => applyRotate(image, degrees));
41
+ return this;
42
+ }
43
+ blur(sigma = 1) {
44
+ if (!Number.isFinite(sigma) || sigma <= 0)
45
+ throw TypeError("Image blur sigma must be positive");
46
+ this.operations.push((image) => applyBlur(image, sigma));
47
+ return this;
48
+ }
49
+ withMetadata() {
50
+ throw Error("Native image transforms strip metadata for privacy");
51
+ }
52
+ async toBuffer() {
53
+ let image = await decode(this.input);
54
+ for (const operation of this.operations)
55
+ image = operation(image);
56
+ return Buffer.from(await encode(image, this.format, this.options));
57
+ }
58
+ output(format, options) {
59
+ const quality = options.quality ?? 80;
60
+ if (!Number.isFinite(quality) || quality < 1 || quality > 100)
61
+ throw TypeError("Image quality must be between 1 and 100");
62
+ this.format = format;
63
+ this.options = { ...options, quality };
64
+ return this;
13
65
  }
14
66
  }
15
67
  export function transform(pipeline) {
16
68
  return async (input) => {
17
- const sharp = await loadSharp(), bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : input, buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes), img = sharp(buf);
18
- return (await pipeline(img)).toBuffer();
69
+ const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
70
+ return (await pipeline(new Pipeline(bytes))).toBuffer();
19
71
  };
20
72
  }
21
73
  export function avatar(size = 512, quality = 85) {
22
- return transform((img) => img.resize(size, size, { fit: "cover" }).webp({ quality }));
74
+ return transform((image) => image.resize(size, size, { fit: "cover" }).webp({ quality }));
23
75
  }
24
76
  export function resize(width, height, fit = "inside") {
25
- return transform((img) => img.resize(width, height, { fit }));
77
+ return transform((image) => image.resize(width, height, { fit }).webp());
26
78
  }
27
- export function stripMetadata() {
28
- return transform((img) => img);
79
+ export function stripMetadata(format = "webp") {
80
+ return transform((image) => image[format]());
29
81
  }
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export { serveFile } from './static-serve';
36
36
  export { Storage, StorageManager } from './facade';
37
37
  export { UploadedFile, uploadedFile, uploadedFiles } from './uploaded-file';
38
38
  // Filesystem type helpers
39
- export { configFromEnv, localDisk, s3Disk } from './types/filesystem';
39
+ export { backblazeDisk, configFromEnv, filebaseDisk, hetznerDisk, localDisk, r2Disk, s3Disk } from './types/filesystem';
40
40
  // Signed-URL helpers (used by Storage.disk('local').signedUrl())
41
41
  export {
42
42
  clearRevokedSignedStorageTokens,
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ export * from "./drivers";
14
14
  export { serveFile } from "./static-serve";
15
15
  export { Storage, StorageManager } from "./facade";
16
16
  export { UploadedFile, uploadedFile, uploadedFiles } from "./uploaded-file";
17
- export { configFromEnv, localDisk, s3Disk } from "./types/filesystem";
17
+ export { backblazeDisk, configFromEnv, filebaseDisk, hetznerDisk, localDisk, r2Disk, s3Disk } from "./types/filesystem";
18
18
  export {
19
19
  clearRevokedSignedStorageTokens,
20
20
  createSignedStorageToken,
@@ -6,6 +6,44 @@ export declare function localDisk(root: string, options?: Partial<Omit<LocalDisk
6
6
  * Helper to create an S3 disk config
7
7
  */
8
8
  export declare function s3Disk(bucket: string, options?: Partial<Omit<S3DiskConfig, 'driver' | 'bucket'>>): S3DiskConfig;
9
+ /**
10
+ * Helper to create a Filebase disk config (stacksjs/stacks#938).
11
+ *
12
+ * Filebase (https://filebase.com) is an S3-compatible, IPFS-backed object
13
+ * store, so it reuses the `s3` adapter with the endpoint pinned to
14
+ * `https://s3.filebase.com` and the region to `us-east-1` (Filebase's single
15
+ * S3 region). Pass a Filebase bucket; supply Filebase credentials via
16
+ * `options.credentials` or the standard `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`
17
+ * env vars. Any field (prefix, visibility, credentials, ...) can be overridden.
18
+ */
19
+ export declare function filebaseDisk(bucket: string, options?: Partial<Omit<S3DiskConfig, 'driver' | 'bucket'>>): S3DiskConfig;
20
+ /**
21
+ * Helper to create a Backblaze B2 disk config (stacksjs/stacks#1897).
22
+ *
23
+ * Backblaze B2 exposes an S3-compatible API, so it reuses the `s3` adapter.
24
+ * `region` is the B2 region embedded in the endpoint (e.g. `us-west-004`,
25
+ * `eu-central-003`); the endpoint resolves to `https://s3.<region>.backblazeb2.com`.
26
+ * Supply B2 application key credentials via `options.credentials` or the AWS_* env vars.
27
+ */
28
+ export declare function backblazeDisk(bucket: string, region: string, options?: Partial<Omit<S3DiskConfig, 'driver' | 'bucket'>>): S3DiskConfig;
29
+ /**
30
+ * Helper to create a Cloudflare R2 disk config (stacksjs/stacks#1896).
31
+ *
32
+ * R2 exposes an S3-compatible API, so it reuses the `s3` adapter. R2 has a
33
+ * single logical region (`auto`) and a per-account endpoint
34
+ * `https://<accountId>.r2.cloudflarestorage.com`. Supply R2 token credentials
35
+ * via `options.credentials` or the AWS_* env vars.
36
+ */
37
+ export declare function r2Disk(bucket: string, accountId: string, options?: Partial<Omit<S3DiskConfig, 'driver' | 'bucket'>>): S3DiskConfig;
38
+ /**
39
+ * Helper to create a Hetzner Object Storage disk config (stacksjs/stacks#1897).
40
+ *
41
+ * Hetzner Object Storage exposes an S3-compatible API, so it reuses the `s3`
42
+ * adapter. `location` is the datacenter (e.g. `fsn1`, `nbg1`, `hel1`), used as
43
+ * both the region and the endpoint host `https://<location>.your-objectstorage.com`.
44
+ * Supply Hetzner S3 credentials via `options.credentials` or the AWS_* env vars.
45
+ */
46
+ export declare function hetznerDisk(bucket: string, location: string, options?: Partial<Omit<S3DiskConfig, 'driver' | 'bucket'>>): S3DiskConfig;
9
47
  /**
10
48
  * Create filesystem config from environment variables
11
49
  */
@@ -15,6 +15,46 @@ export function s3Disk(bucket, options) {
15
15
  ...options
16
16
  };
17
17
  }
18
+ export function filebaseDisk(bucket, options) {
19
+ return {
20
+ driver: "s3",
21
+ bucket,
22
+ region: "us-east-1",
23
+ endpoint: "https://s3.filebase.com",
24
+ visibility: "private",
25
+ ...options
26
+ };
27
+ }
28
+ export function backblazeDisk(bucket, region, options) {
29
+ return {
30
+ driver: "s3",
31
+ bucket,
32
+ region,
33
+ endpoint: `https://s3.${region}.backblazeb2.com`,
34
+ visibility: "private",
35
+ ...options
36
+ };
37
+ }
38
+ export function r2Disk(bucket, accountId, options) {
39
+ return {
40
+ driver: "s3",
41
+ bucket,
42
+ region: "auto",
43
+ endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
44
+ visibility: "private",
45
+ ...options
46
+ };
47
+ }
48
+ export function hetznerDisk(bucket, location, options) {
49
+ return {
50
+ driver: "s3",
51
+ bucket,
52
+ region: location,
53
+ endpoint: `https://${location}.your-objectstorage.com`,
54
+ visibility: "private",
55
+ ...options
56
+ };
57
+ }
18
58
  export function configFromEnv(base = {}) {
19
59
  const env = process.env;
20
60
  return {
package/dist/types.d.ts CHANGED
@@ -178,6 +178,8 @@ export declare interface StorageAdapterConfig {
178
178
  bucket?: string
179
179
  region?: string
180
180
  prefix?: string
181
+ endpoint?: string
182
+ usePathStyleEndpoint?: boolean
181
183
  credentials?: {
182
184
  accessKeyId: string
183
185
  secretAccessKey: string
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/storage",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.161",
5
+ "version": "0.70.163",
6
6
  "description": "The Stacks file system.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -52,23 +52,15 @@
52
52
  "prepublishOnly": "bun run build"
53
53
  },
54
54
  "dependencies": {
55
- "@stacksjs/ts-cloud": "^0.7.49"
56
- },
57
- "peerDependencies": {
58
- "sharp": "^0.35.3"
59
- },
60
- "peerDependenciesMeta": {
61
- "sharp": {
62
- "optional": true
63
- }
55
+ "@stacksjs/ts-cloud": "^0.7.56",
56
+ "ts-images": "^0.1.11"
64
57
  },
65
58
  "devDependencies": {
66
- "@stacksjs/arrays": "0.70.161",
59
+ "@stacksjs/arrays": "0.70.163",
67
60
  "better-dx": "^0.2.17",
68
- "@stacksjs/error-handling": "0.70.161",
69
- "@stacksjs/path": "0.70.161",
70
- "@stacksjs/strings": "0.70.161",
71
- "@stacksjs/types": "0.70.161",
72
- "sharp": "^0.35.3"
61
+ "@stacksjs/error-handling": "0.70.163",
62
+ "@stacksjs/path": "0.70.163",
63
+ "@stacksjs/strings": "0.70.163",
64
+ "@stacksjs/types": "0.70.163"
73
65
  }
74
66
  }