@immagin/client 0.2.2 → 0.2.5

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,12 +17,12 @@ 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
28
  await client.images.upload(buffer, 'photos/hero.jpg')
@@ -30,62 +30,65 @@ await client.images.upload(buffer, 'photos/hero.jpg')
30
30
 
31
31
  ## Images
32
32
 
33
- ### Get a processed URL (via API)
33
+ ### Generate image URLs
34
34
 
35
- 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.
36
36
 
37
37
  ```ts
38
+ // Original image
38
39
  const url = await client.images.url('photo.jpg')
39
- ```
40
-
41
- With transformations:
42
-
43
- ```ts
44
- const url = await client.images.url('photo.jpg', {
45
- size: [400, 300],
46
- text: {
47
- text: 'Hello',
48
- position: 'bottom-right', // top-left, top-right, bottom-left, bottom-right, center
49
- fontSize: 24,
50
- color: '#ffffff',
51
- opacity: 0.8,
52
- },
53
- })
54
- ```
55
-
56
- ### Sign URLs locally (without API call)
57
-
58
- Generate signed image URLs on the server without making an API request. Requires `tenantId` and `tenantSecret` in the constructor.
59
-
60
- ```ts
61
- const client = new Immagin({
62
- apiKey: 'imk_...',
63
- tenantId: 'your-tenant-id',
64
- tenantSecret: 'your-tenant-secret',
65
- })
66
-
67
- // Synchronous - no network request
68
- const url = client.images.signedUrl('photo.jpg')
69
40
 
70
41
  // With transformations
71
- const thumb = client.images.signedUrl('photo.jpg', {
72
- size: [800, 600],
73
- text: { text: '© My Company', position: 'bottom-right', opacity: 0.5 },
74
- })
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
+ // Disable auto-orient (enabled by default)
67
+ const raw = await client.images.url('photo.jpg', [
68
+ { autoOrient: false },
69
+ { resize: { width: 800 } },
70
+ ])
71
+
72
+ // Custom output format (default is WebP at quality 90)
73
+ const jpeg = await client.images.url(
74
+ 'photo.jpg',
75
+ [{ resize: { width: 800 } }],
76
+ { format: 'jpeg', quality: 85 },
77
+ )
75
78
  ```
76
79
 
77
- > **Note:** This uses `node:crypto` and is intended for server-side use only. Never expose your tenant secret in client-side code.
80
+ > **Note:** `url()` uses `node:crypto` for signing and is intended for server-side use only. Tenant credentials are auto-fetched and cached on initialization.
78
81
 
79
- ### Get a pre-signed upload URL
82
+ ### Get a signed upload URL
80
83
 
81
- 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.
84
+ 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.
82
85
 
83
86
  ```ts
84
- // Server: get the presigned URL
87
+ // Server: get the signed upload URL
85
88
  const { uploadUrl, key } = await client.images.signUrl('photos/hero.jpg')
86
89
  // Return uploadUrl to the browser
87
90
 
88
- // Browser: upload directly to S3
91
+ // Browser: upload directly
89
92
  await fetch(uploadUrl, {
90
93
  method: 'PUT',
91
94
  body: file,
@@ -94,7 +97,7 @@ await fetch(uploadUrl, {
94
97
 
95
98
  ### Upload (Node.js)
96
99
 
97
- Convenience method that gets a signed URL and uploads in one call. The file goes directly to S3 and never passes through the API.
100
+ 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.
98
101
 
99
102
  ```ts
100
103
  import { readFileSync } from 'node:fs'
@@ -123,6 +126,27 @@ const page = await client.images.list({
123
126
  await client.images.delete('uploads/photo.jpg')
124
127
  ```
125
128
 
129
+ ### Metadata
130
+
131
+ Inspect image properties (dimensions, format, color space, etc.) without downloading the full image. The request goes through Luna, which extracts metadata using Sharp.
132
+
133
+ ```ts
134
+ const meta = await client.images.metadata('photo.jpg')
135
+ // {
136
+ // width: 1920,
137
+ // height: 1080,
138
+ // format: 'jpeg',
139
+ // space: 'srgb',
140
+ // channels: 3,
141
+ // hasAlpha: false,
142
+ // density: 72,
143
+ // isProgressive: false,
144
+ // size: 204800,
145
+ // }
146
+ ```
147
+
148
+ > **Note:** `metadata()` uses `node:crypto` for signing and is intended for server-side use only.
149
+
126
150
  ## API Keys
127
151
 
128
152
  Manage API keys programmatically.
@@ -171,8 +195,6 @@ try {
171
195
  const client = new Immagin({
172
196
  apiKey: 'imk_...', // Required
173
197
  baseUrl: 'https://...', // Optional, defaults to https://gateway.immag.in
174
- tenantId: 'your-tenant-id', // Optional, required for signedUrl()
175
- tenantSecret: 'your-secret', // Optional, required for signedUrl()
176
198
  })
177
199
  ```
178
200
 
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,11 +8,113 @@ 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;
23
+ }
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
+ autoOrient: boolean;
97
+ };
98
+ type OutputFormat = 'webp' | 'jpeg' | 'png' | 'gif' | 'jp2' | 'tiff' | 'avif' | 'heif';
99
+ interface OutputOptions {
100
+ format?: OutputFormat;
101
+ quality?: number;
102
+ progressive?: boolean;
103
+ lossless?: boolean;
104
+ }
105
+ interface ImageMetadata {
106
+ width: number;
107
+ height: number;
108
+ format: string;
109
+ space: string;
110
+ channels: number;
111
+ hasAlpha: boolean;
112
+ orientation?: number;
113
+ density?: number;
114
+ isProgressive?: boolean;
115
+ pages?: number;
116
+ size: number;
14
117
  }
15
- interface ImageUrlOptions extends ImageEdits {}
16
118
  interface UploadResult {
17
119
  uploadUrl: string;
18
120
  key: string;
@@ -43,6 +145,10 @@ interface CreateKeyResult {
43
145
  prefix: string;
44
146
  name: string;
45
147
  }
148
+ interface ProjectInfo {
149
+ tenantId: string;
150
+ tenantSecret: string;
151
+ }
46
152
  interface ImmaginConfig {
47
153
  apiKey: string;
48
154
  baseUrl?: string;
@@ -54,12 +160,12 @@ interface ImmaginConfig {
54
160
  declare class ImagesResource {
55
161
  private client;
56
162
  constructor(client: Immagin);
57
- signedUrl(key: string, edits?: ImageEdits): string;
58
- url(key: string, options?: ImageUrlOptions): Promise<string>;
163
+ url(key: string, edits?: ImageOperation | ImageOperation[], output?: OutputOptions): Promise<string>;
59
164
  signUrl(key: string): Promise<UploadResult>;
60
165
  upload(file: Blob | Buffer | ReadableStream, key: string): Promise<UploadResult>;
61
166
  list(options?: ListImagesOptions): Promise<ListImagesResult>;
62
167
  delete(key: string): Promise<void>;
168
+ metadata(key: string): Promise<ImageMetadata>;
63
169
  }
64
170
  //#endregion
65
171
  //#region src/resources/keys.d.ts
@@ -81,10 +187,16 @@ declare class Immagin {
81
187
  tenantId?: string;
82
188
  /** @internal */
83
189
  tenantSecret?: string;
190
+ private _tenantInfoPromise?;
84
191
  images: ImagesResource;
85
192
  keys: KeysResource;
86
193
  constructor(config: ImmaginConfig);
87
194
  /** @internal */
195
+ resolveTenantInfo(): Promise<{
196
+ tenantId: string;
197
+ tenantSecret: string;
198
+ }>;
199
+ /** @internal */
88
200
  request<T>(method: string, path: string, options?: {
89
201
  body?: unknown;
90
202
  params?: Record<string, string>;
@@ -98,4 +210,4 @@ declare class ImmaginError extends Error {
98
210
  constructor(message: string, status: number, body?: unknown | undefined);
99
211
  }
100
212
  //#endregion
101
- export { type ApiKey, type CreateKeyResult, type ImageEdits, type ImageEntry, type ImageUrlOptions, Immagin, type ImmaginConfig, ImmaginError, type ListImagesOptions, type ListImagesResult, type TextOverlay, type TextPosition, type UploadResult };
213
+ 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,5 +1,16 @@
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
4
15
  const MIME_TYPES = {
5
16
  jpg: "image/jpeg",
@@ -42,29 +53,17 @@ var ImagesResource = class {
42
53
  constructor(client) {
43
54
  this.client = client;
44
55
  }
45
- signedUrl(key, edits) {
46
- const { tenantId, tenantSecret } = this.client;
47
- 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();
48
58
  const payload = { key };
49
- 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;
50
64
  const base64 = Buffer.from(JSON.stringify(payload)).toString("base64");
51
65
  return `https://${tenantId}.immag.in/${base64}?sig=${createHmac("sha256", tenantSecret).update(base64).digest("hex").slice(0, 16)}`;
52
66
  }
53
- async url(key, options) {
54
- const params = { key };
55
- if (options?.size) {
56
- params.width = String(options.size[0]);
57
- if (options.size.length === 2) params.height = String(options.size[1]);
58
- }
59
- if (options?.text) {
60
- params["text.text"] = options.text.text;
61
- if (options.text.position) params["text.position"] = options.text.position;
62
- if (options.text.fontSize) params["text.fontSize"] = String(options.text.fontSize);
63
- if (options.text.opacity) params["text.opacity"] = String(options.text.opacity);
64
- if (options.text.color) params["text.color"] = options.text.color;
65
- }
66
- return (await this.client.request("GET", "/v1/images/url", { params })).url;
67
- }
68
67
  async signUrl(key) {
69
68
  const contentType = mimeFromKey(key);
70
69
  return this.client.request("POST", "/v1/images/sign-url", { body: contentType ? {
@@ -95,6 +94,21 @@ var ImagesResource = class {
95
94
  async delete(key) {
96
95
  await this.client.request("DELETE", `/v1/images/${key}`);
97
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
+ }
98
112
  };
99
113
 
100
114
  //#endregion
@@ -114,17 +128,6 @@ var KeysResource = class {
114
128
  }
115
129
  };
116
130
 
117
- //#endregion
118
- //#region src/errors.ts
119
- var ImmaginError = class extends Error {
120
- constructor(message, status, body) {
121
- super(message);
122
- this.status = status;
123
- this.body = body;
124
- this.name = "ImmaginError";
125
- }
126
- };
127
-
128
131
  //#endregion
129
132
  //#region src/client.ts
130
133
  const DEFAULT_BASE_URL = "https://gateway.immag.in";
@@ -135,6 +138,7 @@ var Immagin = class {
135
138
  tenantId;
136
139
  /** @internal */
137
140
  tenantSecret;
141
+ _tenantInfoPromise;
138
142
  images;
139
143
  keys;
140
144
  constructor(config) {
@@ -144,6 +148,23 @@ var Immagin = class {
144
148
  this.tenantSecret = config.tenantSecret;
145
149
  this.images = new ImagesResource(this);
146
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;
147
168
  }
148
169
  /** @internal */
149
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.2",
3
+ "version": "0.2.5",
4
4
  "description": "Node.js and browser client for the Immagin image processing API",
5
5
  "type": "module",
6
6
  "license": "MIT",