@immagin/client 0.2.1 → 0.2.4

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
@@ -17,97 +17,87 @@ import { Immagin } from '@immagin/client'
17
17
 
18
18
  const client = new Immagin({ apiKey: 'imk_...' })
19
19
 
20
- // Get a processed image URL (via API)
21
- const url = await client.images.url('photos/hero.jpg', {
22
- size: [800, 600],
23
- })
20
+ // Generate a signed image URL (auto-fetches tenant credentials)
21
+ const url = await client.images.url('photos/hero.jpg', [
22
+ { resize: { width: 800, height: 600 } },
23
+ ])
24
24
 
25
- // Upload an image
25
+ // Upload an image (auto-detects MIME type)
26
26
  import { readFileSync } from 'node:fs'
27
27
  const buffer = readFileSync('photo.jpg')
28
- await client.images.upload(buffer, {
29
- key: 'photos/hero.jpg',
30
- contentType: 'image/jpeg',
31
- })
28
+ await client.images.upload(buffer, 'photos/hero.jpg')
32
29
  ```
33
30
 
34
31
  ## Images
35
32
 
36
- ### Get a processed URL (via API)
33
+ ### Generate image URLs
37
34
 
38
- Returns a signed URL that serves the image through Immagin's processing pipeline.
35
+ Generate signed image URLs for serving processed images. All URLs are automatically signed — the signature locks transformation parameters so they can't be tampered with.
39
36
 
40
37
  ```ts
38
+ // Original image
41
39
  const url = await client.images.url('photo.jpg')
42
- ```
43
-
44
- With transformations:
45
-
46
- ```ts
47
- const url = await client.images.url('photo.jpg', {
48
- size: [400, 300],
49
- text: {
50
- text: 'Hello',
51
- position: 'bottom-right', // top-left, top-right, bottom-left, bottom-right, center
52
- fontSize: 24,
53
- color: '#ffffff',
54
- opacity: 0.8,
55
- },
56
- })
57
- ```
58
-
59
- ### Sign URLs locally (without API call)
60
-
61
- Generate signed image URLs on the server without making an API request. Requires `tenantId` and `tenantSecret` in the constructor.
62
-
63
- ```ts
64
- const client = new Immagin({
65
- apiKey: 'imk_...',
66
- tenantId: 'your-tenant-id',
67
- tenantSecret: 'your-tenant-secret',
68
- })
69
-
70
- // Synchronous - no network request
71
- const url = client.images.signedUrl('photo.jpg')
72
40
 
73
41
  // With transformations
74
- const thumb = client.images.signedUrl('photo.jpg', {
75
- size: [800, 600],
76
- text: { text: '© My Company', position: 'bottom-right', opacity: 0.5 },
77
- })
42
+ const thumb = await client.images.url('photo.jpg', [
43
+ { resize: { width: 800, height: 600 } },
44
+ { text: { text: '© My Company', position: 'bottom-right', opacity: 0.5 } },
45
+ ])
46
+
47
+ // Rotate, blur, grayscale
48
+ const edited = await client.images.url('photo.jpg', [
49
+ { rotate: { angle: 90 } },
50
+ { blur: 3 },
51
+ { grayscale: true },
52
+ ])
53
+
54
+ // Crop a region
55
+ const cropped = await client.images.url('photo.jpg', [
56
+ { crop: { left: 100, top: 50, width: 400, height: 300 } },
57
+ ])
58
+
59
+ // Flip, sharpen, adjust colors
60
+ const adjusted = await client.images.url('photo.jpg', [
61
+ { flip: true },
62
+ { sharpen: 2 },
63
+ { modulate: { brightness: 1.2, saturation: 0.8 } },
64
+ ])
65
+
66
+ // Custom output format (default is WebP at quality 90)
67
+ const jpeg = await client.images.url(
68
+ 'photo.jpg',
69
+ [{ resize: { width: 800 } }],
70
+ { format: 'jpeg', quality: 85 },
71
+ )
78
72
  ```
79
73
 
80
- > **Note:** This uses `node:crypto` and is intended for server-side use only. Never expose your tenant secret in client-side code.
74
+ > **Note:** `url()` uses `node:crypto` for signing and is intended for server-side use only. Tenant credentials are auto-fetched and cached on initialization.
81
75
 
82
- ### Get a pre-signed upload URL
76
+ ### Get a signed upload URL
83
77
 
84
- Returns a pre-signed S3 URL for direct upload (expires in 5 minutes). Useful for browser uploads where you don't want to expose your API key.
78
+ Returns a CloudFront signed URL for direct upload (expires in 5 minutes). Useful for browser uploads where you don't want to expose your API key. The URL points to a CloudFront distribution (not S3 directly), so the underlying infrastructure is not exposed.
85
79
 
86
80
  ```ts
87
- // Server: get the presigned URL
88
- const { uploadUrl, key } = await client.images.signUrl('photos/hero.jpg', 'image/jpeg')
81
+ // Server: get the signed upload URL
82
+ const { uploadUrl, key } = await client.images.signUrl('photos/hero.jpg')
89
83
  // Return uploadUrl to the browser
90
84
 
91
- // Browser: upload directly to S3
85
+ // Browser: upload directly
92
86
  await fetch(uploadUrl, {
93
87
  method: 'PUT',
94
88
  body: file,
95
- headers: { 'content-type': 'image/jpeg' },
96
89
  })
97
90
  ```
98
91
 
99
92
  ### Upload (Node.js)
100
93
 
101
- Convenience method that gets a signed URL and uploads in one call. The file goes directly to S3 and never passes through the API.
94
+ Convenience method that gets a signed URL and uploads in one call. The file goes directly to CloudFront/S3 and never passes through the API. MIME type is auto-detected from file bytes (magic bytes for JPEG, PNG, GIF, WebP, BMP, AVIF, HEIC, HEIF) or file extension.
102
95
 
103
96
  ```ts
104
97
  import { readFileSync } from 'node:fs'
105
98
 
106
99
  const buffer = readFileSync('photo.jpg')
107
- await client.images.upload(buffer, {
108
- key: 'uploads/photo.jpg',
109
- contentType: 'image/jpeg',
110
- })
100
+ await client.images.upload(buffer, 'uploads/photo.jpg')
111
101
  ```
112
102
 
113
103
  ### List
@@ -130,6 +120,27 @@ const page = await client.images.list({
130
120
  await client.images.delete('uploads/photo.jpg')
131
121
  ```
132
122
 
123
+ ### Metadata
124
+
125
+ Inspect image properties (dimensions, format, color space, etc.) without downloading the full image. The request goes through Luna, which extracts metadata using Sharp.
126
+
127
+ ```ts
128
+ const meta = await client.images.metadata('photo.jpg')
129
+ // {
130
+ // width: 1920,
131
+ // height: 1080,
132
+ // format: 'jpeg',
133
+ // space: 'srgb',
134
+ // channels: 3,
135
+ // hasAlpha: false,
136
+ // density: 72,
137
+ // isProgressive: false,
138
+ // size: 204800,
139
+ // }
140
+ ```
141
+
142
+ > **Note:** `metadata()` uses `node:crypto` for signing and is intended for server-side use only.
143
+
133
144
  ## API Keys
134
145
 
135
146
  Manage API keys programmatically.
@@ -178,8 +189,6 @@ try {
178
189
  const client = new Immagin({
179
190
  apiKey: 'imk_...', // Required
180
191
  baseUrl: 'https://...', // Optional, defaults to https://gateway.immag.in
181
- tenantId: 'your-tenant-id', // Optional, required for signedUrl()
182
- tenantSecret: 'your-secret', // Optional, required for signedUrl()
183
192
  })
184
193
  ```
185
194
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/types.d.ts
2
- type TextPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
2
+ type TextPosition = 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
3
3
  interface TextOverlay {
4
4
  text: string;
5
5
  fontSize?: number;
@@ -8,14 +8,110 @@ interface TextOverlay {
8
8
  position?: TextPosition;
9
9
  padding?: number;
10
10
  }
11
- interface ImageEdits {
12
- size?: [number] | [number, number];
13
- text?: TextOverlay;
11
+ type ResizeFit = 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
12
+ type ResizePosition = 'top' | 'right top' | 'right' | 'right bottom' | 'bottom' | 'left bottom' | 'left' | 'left top' | 'centre' | 'center' | 'entropy' | 'attention';
13
+ type ResizeKernel = 'nearest' | 'cubic' | 'mitchell' | 'lanczos2' | 'lanczos3';
14
+ interface ResizeOptions {
15
+ width: number;
16
+ height?: number;
17
+ fit?: ResizeFit;
18
+ position?: ResizePosition;
19
+ kernel?: ResizeKernel;
20
+ background?: string;
21
+ upscale?: boolean;
22
+ downscale?: boolean;
14
23
  }
15
- interface ImageUrlOptions extends ImageEdits {}
16
- interface UploadOptions {
17
- key: string;
18
- contentType?: string;
24
+ interface RotateOptions {
25
+ angle: number;
26
+ background?: string;
27
+ }
28
+ interface CropOptions {
29
+ left: number;
30
+ top: number;
31
+ width: number;
32
+ height: number;
33
+ }
34
+ interface ExtendOptions {
35
+ top?: number;
36
+ bottom?: number;
37
+ left?: number;
38
+ right?: number;
39
+ background?: string;
40
+ }
41
+ interface ModulateOptions {
42
+ brightness?: number;
43
+ saturation?: number;
44
+ hue?: number;
45
+ lightness?: number;
46
+ }
47
+ interface FlattenOptions {
48
+ background?: string;
49
+ }
50
+ interface TintOptions {
51
+ r: number;
52
+ g: number;
53
+ b: number;
54
+ }
55
+ interface NormalizeOptions {
56
+ lower?: number;
57
+ upper?: number;
58
+ }
59
+ interface TrimOptions {
60
+ background?: string;
61
+ threshold?: number;
62
+ }
63
+ type ImageOperation = {
64
+ resize: ResizeOptions;
65
+ } | {
66
+ text: TextOverlay;
67
+ } | {
68
+ rotate: RotateOptions;
69
+ } | {
70
+ flip: true;
71
+ } | {
72
+ flop: true;
73
+ } | {
74
+ blur: number;
75
+ } | {
76
+ sharpen: number;
77
+ } | {
78
+ grayscale: true;
79
+ } | {
80
+ crop: CropOptions;
81
+ } | {
82
+ extend: ExtendOptions;
83
+ } | {
84
+ modulate: ModulateOptions;
85
+ } | {
86
+ flatten: FlattenOptions;
87
+ } | {
88
+ tint: TintOptions;
89
+ } | {
90
+ invert: boolean;
91
+ } | {
92
+ normalize: NormalizeOptions;
93
+ } | {
94
+ trim: TrimOptions;
95
+ };
96
+ type OutputFormat = 'webp' | 'jpeg' | 'png' | 'gif' | 'jp2' | 'tiff' | 'avif' | 'heif';
97
+ interface OutputOptions {
98
+ format?: OutputFormat;
99
+ quality?: number;
100
+ progressive?: boolean;
101
+ lossless?: boolean;
102
+ }
103
+ interface ImageMetadata {
104
+ width: number;
105
+ height: number;
106
+ format: string;
107
+ space: string;
108
+ channels: number;
109
+ hasAlpha: boolean;
110
+ orientation?: number;
111
+ density?: number;
112
+ isProgressive?: boolean;
113
+ pages?: number;
114
+ size: number;
19
115
  }
20
116
  interface UploadResult {
21
117
  uploadUrl: string;
@@ -47,6 +143,10 @@ interface CreateKeyResult {
47
143
  prefix: string;
48
144
  name: string;
49
145
  }
146
+ interface ProjectInfo {
147
+ tenantId: string;
148
+ tenantSecret: string;
149
+ }
50
150
  interface ImmaginConfig {
51
151
  apiKey: string;
52
152
  baseUrl?: string;
@@ -58,12 +158,12 @@ interface ImmaginConfig {
58
158
  declare class ImagesResource {
59
159
  private client;
60
160
  constructor(client: Immagin);
61
- signedUrl(key: string, edits?: ImageEdits): string;
62
- url(key: string, options?: ImageUrlOptions): Promise<string>;
63
- signUrl(key: string, contentType?: string): Promise<UploadResult>;
64
- upload(file: Blob | Buffer | ReadableStream, options: UploadOptions): Promise<UploadResult>;
161
+ url(key: string, edits?: ImageOperation | ImageOperation[], output?: OutputOptions): Promise<string>;
162
+ signUrl(key: string): Promise<UploadResult>;
163
+ upload(file: Blob | Buffer | ReadableStream, key: string): Promise<UploadResult>;
65
164
  list(options?: ListImagesOptions): Promise<ListImagesResult>;
66
165
  delete(key: string): Promise<void>;
166
+ metadata(key: string): Promise<ImageMetadata>;
67
167
  }
68
168
  //#endregion
69
169
  //#region src/resources/keys.d.ts
@@ -85,10 +185,16 @@ declare class Immagin {
85
185
  tenantId?: string;
86
186
  /** @internal */
87
187
  tenantSecret?: string;
188
+ private _tenantInfoPromise?;
88
189
  images: ImagesResource;
89
190
  keys: KeysResource;
90
191
  constructor(config: ImmaginConfig);
91
192
  /** @internal */
193
+ resolveTenantInfo(): Promise<{
194
+ tenantId: string;
195
+ tenantSecret: string;
196
+ }>;
197
+ /** @internal */
92
198
  request<T>(method: string, path: string, options?: {
93
199
  body?: unknown;
94
200
  params?: Record<string, string>;
@@ -102,4 +208,4 @@ declare class ImmaginError extends Error {
102
208
  constructor(message: string, status: number, body?: unknown | undefined);
103
209
  }
104
210
  //#endregion
105
- export { type ApiKey, type CreateKeyResult, type ImageEdits, type ImageEntry, type ImageUrlOptions, Immagin, type ImmaginConfig, ImmaginError, type ListImagesOptions, type ListImagesResult, type TextOverlay, type TextPosition, type UploadOptions, type UploadResult };
211
+ export { type ApiKey, type CreateKeyResult, type CropOptions, type ExtendOptions, type FlattenOptions, type ImageEntry, type ImageMetadata, type ImageOperation, Immagin, type ImmaginConfig, ImmaginError, type ListImagesOptions, type ListImagesResult, type ModulateOptions, type NormalizeOptions, type OutputFormat, type OutputOptions, type ProjectInfo, type ResizeFit, type ResizeKernel, type ResizeOptions, type ResizePosition, type RotateOptions, type TextOverlay, type TextPosition, type TintOptions, type TrimOptions, type UploadResult };
package/dist/index.mjs CHANGED
@@ -1,42 +1,82 @@
1
1
  import { createHmac } from "node:crypto";
2
2
 
3
+ //#region src/errors.ts
4
+ var ImmaginError = class extends Error {
5
+ constructor(message, status, body) {
6
+ super(message);
7
+ this.status = status;
8
+ this.body = body;
9
+ this.name = "ImmaginError";
10
+ }
11
+ };
12
+
13
+ //#endregion
3
14
  //#region src/resources/images.ts
15
+ const MIME_TYPES = {
16
+ jpg: "image/jpeg",
17
+ jpeg: "image/jpeg",
18
+ png: "image/png",
19
+ gif: "image/gif",
20
+ webp: "image/webp",
21
+ avif: "image/avif",
22
+ svg: "image/svg+xml",
23
+ tiff: "image/tiff",
24
+ tif: "image/tiff",
25
+ bmp: "image/bmp",
26
+ ico: "image/x-icon",
27
+ heic: "image/heic",
28
+ heif: "image/heif"
29
+ };
30
+ function mimeFromKey(key) {
31
+ const ext = key.split(".").pop()?.toLowerCase();
32
+ return ext ? MIME_TYPES[ext] : void 0;
33
+ }
34
+ function mimeFromBuffer(buf) {
35
+ if (buf.length < 12) return void 0;
36
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
37
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
38
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
39
+ if (buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) return "image/webp";
40
+ if (buf[0] === 66 && buf[1] === 77) return "image/bmp";
41
+ if (buf[4] === 102 && buf[5] === 116 && buf[6] === 121 && buf[7] === 112) {
42
+ const brand = buf.slice(8, 12).toString("ascii");
43
+ if (brand === "avif" || brand === "avis") return "image/avif";
44
+ if (brand === "heic" || brand === "heix") return "image/heic";
45
+ if (brand === "heif" || brand === "mif1") return "image/heif";
46
+ }
47
+ }
48
+ function mimeFromFile(file) {
49
+ if (typeof Blob !== "undefined" && file instanceof Blob && file.type) return file.type;
50
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(file)) return mimeFromBuffer(file);
51
+ }
4
52
  var ImagesResource = class {
5
53
  constructor(client) {
6
54
  this.client = client;
7
55
  }
8
- signedUrl(key, edits) {
9
- const { tenantId, tenantSecret } = this.client;
10
- if (!tenantId || !tenantSecret) throw new Error("tenantId and tenantSecret are required for signedUrl(). Pass them in the Immagin constructor.");
56
+ async url(key, edits, output) {
57
+ const { tenantId, tenantSecret } = await this.client.resolveTenantInfo();
11
58
  const payload = { key };
12
- if (edits && Object.keys(edits).length > 0) payload.edits = edits;
59
+ if (edits) {
60
+ const editsArray = Array.isArray(edits) ? edits : [edits];
61
+ if (editsArray.length > 0) payload.edits = editsArray;
62
+ }
63
+ if (output) payload.output = output;
13
64
  const base64 = Buffer.from(JSON.stringify(payload)).toString("base64");
14
65
  return `https://${tenantId}.immag.in/${base64}?sig=${createHmac("sha256", tenantSecret).update(base64).digest("hex").slice(0, 16)}`;
15
66
  }
16
- async url(key, options) {
17
- const params = { key };
18
- if (options?.size) {
19
- params.width = String(options.size[0]);
20
- if (options.size.length === 2) params.height = String(options.size[1]);
21
- }
22
- if (options?.text) {
23
- params["text.text"] = options.text.text;
24
- if (options.text.position) params["text.position"] = options.text.position;
25
- if (options.text.fontSize) params["text.fontSize"] = String(options.text.fontSize);
26
- if (options.text.opacity) params["text.opacity"] = String(options.text.opacity);
27
- if (options.text.color) params["text.color"] = options.text.color;
28
- }
29
- return (await this.client.request("GET", "/v1/images/url", { params })).url;
30
- }
31
- async signUrl(key, contentType) {
32
- return this.client.request("POST", "/v1/images/sign-url", { body: {
67
+ async signUrl(key) {
68
+ const contentType = mimeFromKey(key);
69
+ return this.client.request("POST", "/v1/images/sign-url", { body: contentType ? {
33
70
  key,
34
- contentType: contentType || "application/octet-stream"
35
- } });
71
+ contentType
72
+ } : { key } });
36
73
  }
37
- async upload(file, options) {
38
- const { key, contentType } = options;
39
- const signResult = await this.signUrl(key, contentType);
74
+ async upload(file, key) {
75
+ const contentType = mimeFromFile(file) ?? mimeFromKey(key);
76
+ const signResult = await this.client.request("POST", "/v1/images/sign-url", { body: contentType ? {
77
+ key,
78
+ contentType
79
+ } : { key } });
40
80
  await fetch(signResult.uploadUrl, {
41
81
  method: "PUT",
42
82
  body: file,
@@ -54,6 +94,21 @@ var ImagesResource = class {
54
94
  async delete(key) {
55
95
  await this.client.request("DELETE", `/v1/images/${key}`);
56
96
  }
97
+ async metadata(key) {
98
+ const { tenantId, tenantSecret } = await this.client.resolveTenantInfo();
99
+ const payload = JSON.stringify({
100
+ key,
101
+ metadata: true
102
+ });
103
+ const base64 = Buffer.from(payload).toString("base64");
104
+ const url = `https://${tenantId}.immag.in/${base64}?sig=${createHmac("sha256", tenantSecret).update(base64).digest("hex").slice(0, 16)}`;
105
+ const response = await fetch(url);
106
+ if (!response.ok) {
107
+ const body = await response.text();
108
+ throw new ImmaginError(`HTTP ${response.status}`, response.status, body);
109
+ }
110
+ return response.json();
111
+ }
57
112
  };
58
113
 
59
114
  //#endregion
@@ -73,17 +128,6 @@ var KeysResource = class {
73
128
  }
74
129
  };
75
130
 
76
- //#endregion
77
- //#region src/errors.ts
78
- var ImmaginError = class extends Error {
79
- constructor(message, status, body) {
80
- super(message);
81
- this.status = status;
82
- this.body = body;
83
- this.name = "ImmaginError";
84
- }
85
- };
86
-
87
131
  //#endregion
88
132
  //#region src/client.ts
89
133
  const DEFAULT_BASE_URL = "https://gateway.immag.in";
@@ -94,6 +138,7 @@ var Immagin = class {
94
138
  tenantId;
95
139
  /** @internal */
96
140
  tenantSecret;
141
+ _tenantInfoPromise;
97
142
  images;
98
143
  keys;
99
144
  constructor(config) {
@@ -103,6 +148,23 @@ var Immagin = class {
103
148
  this.tenantSecret = config.tenantSecret;
104
149
  this.images = new ImagesResource(this);
105
150
  this.keys = new KeysResource(this);
151
+ if (!this.tenantId || !this.tenantSecret) this.resolveTenantInfo().catch(() => {});
152
+ }
153
+ /** @internal */
154
+ async resolveTenantInfo() {
155
+ if (this.tenantId && this.tenantSecret) return {
156
+ tenantId: this.tenantId,
157
+ tenantSecret: this.tenantSecret
158
+ };
159
+ if (!this._tenantInfoPromise) this._tenantInfoPromise = this.request("GET", "/v1/project").then((info) => {
160
+ this.tenantId = info.tenantId;
161
+ this.tenantSecret = info.tenantSecret;
162
+ return info;
163
+ }).catch((err) => {
164
+ this._tenantInfoPromise = void 0;
165
+ throw err;
166
+ });
167
+ return this._tenantInfoPromise;
106
168
  }
107
169
  /** @internal */
108
170
  async request(method, path, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immagin/client",
3
- "version": "0.2.1",
3
+ "version": "0.2.4",
4
4
  "description": "Node.js and browser client for the Immagin image processing API",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,15 +34,14 @@
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  },
37
- "scripts": {
38
- "build": "tsdown",
39
- "dev": "tsdown --watch",
40
- "test": "vitest run",
41
- "prepublishOnly": "tsdown"
42
- },
43
37
  "devDependencies": {
44
38
  "tsdown": "^0.20.3",
45
39
  "typescript": "^5.7.2",
46
40
  "vitest": "^3.0.5"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "dev": "tsdown --watch",
45
+ "test": "vitest run"
47
46
  }
48
- }
47
+ }